diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index c12417a..3c78c59 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -450,3 +450,8 @@ - Added playback speed switch for voice messages (`1.0x -> 1.5x -> 2.0x`). - Added view-only circle video renderer for `video_note` messages with looped playback. - Kept regular audio/video attachment rendering for non-voice/non-circle media unchanged. + +### Step 72 - Adaptive layout baseline (phone/tablet) + voice release fix +- Added tablet-aware max-width layout constraints across major screens (login, verify/reset auth, chats list, chat, profile, settings). +- Kept phone layout unchanged while centering content and limiting line width on larger displays. +- Fixed voice hold-to-send gesture reliability by removing pointer-input restarts during active recording, so release consistently triggers send path. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/LoginScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/LoginScreen.kt index 569d936..c66e12b 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/LoginScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/LoginScreen.kt @@ -1,9 +1,11 @@ package ru.daemonlord.messenger.ui.auth import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.WindowInsets @@ -17,6 +19,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp @@ -29,14 +32,21 @@ fun LoginScreen( onOpenVerifyEmail: () -> Unit, onOpenResetPassword: () -> Unit, ) { - Column( + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 + Box( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) .padding(horizontal = 24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + contentAlignment = Alignment.Center, ) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (isTabletLayout) Modifier.widthIn(max = 560.dp) else Modifier), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { Text( text = "Messenger Login", style = MaterialTheme.typography.headlineSmall, @@ -102,5 +112,6 @@ fun LoginScreen( ) { Text(text = "Forgot password") } + } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/reset/ResetPasswordScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/reset/ResetPasswordScreen.kt index 4aac86e..4ebfad9 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/reset/ResetPasswordScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/reset/ResetPasswordScreen.kt @@ -1,12 +1,14 @@ package ru.daemonlord.messenger.ui.auth.reset import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator @@ -18,7 +20,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -33,13 +37,20 @@ fun ResetPasswordRoute( val state by viewModel.uiState.collectAsStateWithLifecycle() var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } - Column( + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 + Box( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + contentAlignment = Alignment.TopCenter, ) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (isTabletLayout) Modifier.widthIn(max = 620.dp) else Modifier), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { Text("Password reset", style = MaterialTheme.typography.headlineSmall) OutlinedTextField( value = email, @@ -83,5 +94,6 @@ fun ResetPasswordRoute( Button(onClick = onBackToLogin, modifier = Modifier.fillMaxWidth()) { Text("Back to login") } + } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/verify/VerifyEmailScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/verify/VerifyEmailScreen.kt index 06fe5f5..169b608 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/verify/VerifyEmailScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/auth/verify/VerifyEmailScreen.kt @@ -1,12 +1,14 @@ package ru.daemonlord.messenger.ui.auth.verify import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator @@ -19,7 +21,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -40,13 +44,20 @@ fun VerifyEmailRoute( } } - Column( + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 + Box( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + contentAlignment = Alignment.TopCenter, ) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (isTabletLayout) Modifier.widthIn(max = 620.dp) else Modifier), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { Text("Verify email", style = MaterialTheme.typography.headlineSmall) OutlinedTextField( value = editableToken, @@ -73,5 +84,6 @@ fun VerifyEmailRoute( Button(onClick = onBackToLogin, modifier = Modifier.fillMaxWidth()) { Text("Back to login") } + } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 2084b10..946e314 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -237,6 +238,8 @@ fun ChatScreen( var showDeleteDialog by remember { mutableStateOf(false) } val actionSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val forwardSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 + val adaptiveHorizontalPadding = if (isTabletLayout) 72.dp else 0.dp LaunchedEffect(state.isRecordingVoice) { if (!state.isRecordingVoice) return@LaunchedEffect @@ -264,7 +267,8 @@ fun ChatScreen( ), ), ) - .windowInsetsPadding(WindowInsets.safeDrawing), + .windowInsetsPadding(WindowInsets.safeDrawing) + .padding(horizontal = adaptiveHorizontalPadding), ) { Row( modifier = Modifier @@ -676,7 +680,6 @@ fun ChatScreen( } else { VoiceHoldToRecordButton( enabled = state.canSendMessages && !state.isUploadingMedia, - isRecording = state.isRecordingVoice, isLocked = state.isVoiceLocked, onStart = onVoiceRecordStart, onLock = onVoiceRecordLock, @@ -1357,7 +1360,6 @@ private fun VoiceWaveform( @Composable private fun VoiceHoldToRecordButton( enabled: Boolean, - isRecording: Boolean, isLocked: Boolean, onStart: () -> Unit, onLock: () -> Unit, @@ -1366,7 +1368,7 @@ private fun VoiceHoldToRecordButton( ) { Box( modifier = Modifier - .pointerInput(enabled, isRecording, isLocked) { + .pointerInput(enabled, isLocked) { if (!enabled || isLocked) return@pointerInput awaitEachGesture { val down = awaitFirstDown(requireUnconsumed = false) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt index e1d556f..933dc7e 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.WindowInsets @@ -41,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -133,12 +135,19 @@ fun ChatListScreen( var selectedManageChatIdText by remember { mutableStateOf("") } var manageUserIdText by remember { mutableStateOf("") } var manageRoleText by remember { mutableStateOf("member") } + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 - Column( + Box( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing), + contentAlignment = Alignment.TopCenter, ) { + Column( + modifier = Modifier + .fillMaxSize() + .then(if (isTabletLayout) Modifier.widthIn(max = 820.dp) else Modifier), + ) { TabRow( selectedTabIndex = if (state.selectedTab == ChatTab.ALL) 0 else 1, ) { @@ -490,6 +499,7 @@ fun ChatListScreen( } } } + } } @Composable diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt index 5b29364..c716282 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -38,6 +39,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -78,6 +80,7 @@ fun ProfileScreen( } val context = androidx.compose.ui.platform.LocalContext.current + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 val scrollState = rememberScrollState() val pickAvatarLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), @@ -92,15 +95,21 @@ fun ProfileScreen( } } - Column( + Box( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) - .verticalScroll(scrollState) - .navigationBarsPadding() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + .navigationBarsPadding(), + contentAlignment = Alignment.TopCenter, ) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (isTabletLayout) Modifier.widthIn(max = 720.dp) else Modifier) + .verticalScroll(scrollState) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { Text( text = "Profile", style = MaterialTheme.typography.headlineSmall, @@ -200,6 +209,7 @@ fun ProfileScreen( ) { Text("Back to chats") } + } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/settings/SettingsScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/settings/SettingsScreen.kt index bc5d466..f51386e 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/settings/SettingsScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/settings/SettingsScreen.kt @@ -1,6 +1,7 @@ package ru.daemonlord.messenger.ui.settings import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -28,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -66,20 +69,27 @@ fun SettingsScreen( var blockUserIdInput by remember { mutableStateOf("") } var nightMode by remember { mutableIntStateOf(AppCompatDelegate.getDefaultNightMode()) } val scrollState = rememberScrollState() + val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840 LaunchedEffect(Unit) { viewModel.refresh() viewModel.refreshRecoveryStatus() } - Column( + Box( modifier = Modifier .fillMaxSize() - .windowInsetsPadding(WindowInsets.safeDrawing) - .verticalScroll(scrollState) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + .windowInsetsPadding(WindowInsets.safeDrawing), + contentAlignment = Alignment.TopCenter, ) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (isTabletLayout) Modifier.widthIn(max = 720.dp) else Modifier) + .verticalScroll(scrollState) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { Text( text = "Settings", style = MaterialTheme.typography.headlineSmall, @@ -310,5 +320,6 @@ fun SettingsScreen( Text("Back to chats") } } + } } } diff --git a/docs/android-checklist.md b/docs/android-checklist.md index b3877e3..9f422f8 100644 --- a/docs/android-checklist.md +++ b/docs/android-checklist.md @@ -97,7 +97,7 @@ ## 13. UI/UX и темы - [x] Светлая/темная тема (читаемая) -- [ ] Адаптивность phone/tablet +- [x] Адаптивность phone/tablet - [x] Контекстные меню без конфликтов жестов - [x] Bottom sheets/dialog behavior consistency - [x] Accessibility (TalkBack, dynamic type)