diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt index 1ffc65f592..8dffb1fd9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt @@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment @AndroidEntryPoint class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { - private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() } + companion object{ + const val PEEK_RATIO = 0.94f + } + + private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index 97740b2a26..c3c33323cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -1,10 +1,13 @@ package org.thoughtcrime.securesms.conversation.start.newmessage -import androidx.compose.animation.AnimatedVisibility +import android.graphics.Rect +import android.os.Build +import android.view.ViewTreeObserver import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import network.loki.messenger.R -import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton +import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO import org.thoughtcrime.securesms.ui.LoadingArcOr -import org.thoughtcrime.securesms.ui.theme.LocalDimensions -import org.thoughtcrime.securesms.ui.theme.PreviewTheme -import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider -import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode @@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import kotlin.math.max private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan) @@ -76,63 +93,113 @@ private fun EnterAccountId( callbacks: Callbacks, onHelp: () -> Unit = {} ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .imePadding() - ) { - Column( - modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), - horizontalAlignment = Alignment.CenterHorizontally, + // the scaffold is required to provide the contentPadding. That contentPadding is needed + // to properly handle the ime padding. + Scaffold() { contentPadding -> + // we need this extra surface to handle nested scrolling properly, + // because this scrollable component is inside a bottomSheet dialog which is itself scrollable + Surface( + modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), + color = LocalColors.current.backgroundSecondary ) { - SessionOutlinedTextField( - text = state.newMessageIdOrOns, - modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing), - contentDescription = "Session id input box", - placeholder = stringResource(R.string.accountIdOrOnsEnter), - onChange = callbacks::onChange, - onContinue = callbacks::onContinue, - error = state.error?.string(), - isTextErrorColor = state.isTextErrorColor - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + var accountModifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) - BorderlessButtonWithIcon( - text = stringResource(R.string.messageNewDescription), - modifier = Modifier - .contentDescription(R.string.AccessibilityId_help_desk_link) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxWidth(), - style = LocalType.current.small, - color = LocalColors.current.textSecondary, - iconRes = R.drawable.ic_circle_question_mark, - onClick = onHelp - ) - } + // There is a known issue with the ime padding on android versions below 30 + // So we these older versions we need to resort to some manual padding based on the visible height + // when the keyboard is up + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + val keyboardHeight by keyboardHeight() + accountModifier = accountModifier.padding(bottom = keyboardHeight) + } else { + accountModifier = accountModifier + .consumeWindowInsets(contentPadding) + .imePadding() + } - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - Spacer(Modifier.weight(2f)) + Column( + modifier = accountModifier + ) { + Column( + modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SessionOutlinedTextField( + text = state.newMessageIdOrOns, + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing), + contentDescription = "Session id input box", + placeholder = stringResource(R.string.accountIdOrOnsEnter), + onChange = callbacks::onChange, + onContinue = callbacks::onContinue, + error = state.error?.string(), + isTextErrorColor = state.isTextErrorColor + ) - PrimaryOutlineButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = LocalDimensions.current.xlargeSpacing) - .padding(bottom = LocalDimensions.current.smallSpacing) - .fillMaxWidth() - .contentDescription(R.string.next), - enabled = state.isNextButtonEnabled, - onClick = callbacks::onContinue - ) { - LoadingArcOr(state.loading) { - Text(stringResource(R.string.next)) + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + + BorderlessButtonWithIcon( + text = stringResource(R.string.messageNewDescription), + modifier = Modifier + .contentDescription(R.string.AccessibilityId_help_desk_link) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxWidth(), + style = LocalType.current.small, + color = LocalColors.current.textSecondary, + iconRes = R.drawable.ic_circle_question_mark, + onClick = onHelp + ) + } + + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + Spacer(Modifier.weight(2f)) + + PrimaryOutlineButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = LocalDimensions.current.xlargeSpacing) + .padding(bottom = LocalDimensions.current.smallSpacing) + .fillMaxWidth() + .contentDescription(R.string.next), + enabled = state.isNextButtonEnabled, + onClick = callbacks::onContinue + ) { + LoadingArcOr(state.loading) { + Text(stringResource(R.string.next)) + } + } } } } } +@Composable +fun keyboardHeight(): MutableState { + val view = LocalView.current + var keyboardHeight = remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + + DisposableEffect(view) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height * PEEK_RATIO + val keypadHeightPx = max( screenHeight - rect.bottom, 0f) + + keyboardHeight.value = with(density) { keypadHeightPx.toDp() } + } + + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + + return keyboardHeight +} + @Preview @Composable private fun PreviewNewMessage(