Fix crash when starting a DM (#6419)

`AnchoredDraggable.requireOffset` was called before it was populated when displaying  `CreateDmConfirmationBottomSheet`, because the keyboard and the bottom sheet were causing conflicting animations related to the insets.

Hiding the keyboard before displaying the bottom sheet seems to fix the issue, and `skipPartiallyExpanded` results in a better UX (and also worked around the issue by itself).
This commit is contained in:
Jorge Martin Espinosa
2026-03-23 16:00:04 +01:00
committed by GitHub
parent 93ab9f43dc
commit 9074692189
3 changed files with 44 additions and 4 deletions

View File

@@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -31,6 +33,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.R
import io.element.android.features.startchat.impl.components.UserListView import io.element.android.features.startchat.impl.components.UserListView
import io.element.android.libraries.androidutils.ui.hideKeyboardAndAwaitAnimation
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@@ -47,6 +50,7 @@ import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBot
import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@Composable @Composable
fun StartChatView( fun StartChatView(
@@ -59,6 +63,8 @@ fun StartChatView(
onRoomDirectorySearchClick: () -> Unit, onRoomDirectorySearchClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val coroutineScope = rememberCoroutineScope()
Scaffold( Scaffold(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
topBar = { topBar = {
@@ -73,6 +79,8 @@ fun StartChatView(
.consumeWindowInsets(paddingValues), .consumeWindowInsets(paddingValues),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
val view = LocalView.current
UserListView( UserListView(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
// Do not render suggestions in this case, the suggestion will be rendered // Do not render suggestions in this case, the suggestion will be rendered
@@ -81,7 +89,10 @@ fun StartChatView(
recentDirectRooms = persistentListOf(), recentDirectRooms = persistentListOf(),
), ),
onSelectUser = { onSelectUser = {
state.eventSink(StartChatEvents.StartDM(it)) coroutineScope.launch {
view.hideKeyboardAndAwaitAnimation()
state.eventSink(StartChatEvents.StartDM(it))
}
}, },
onDeselectUser = { }, onDeselectUser = { },
) )

View File

@@ -9,12 +9,15 @@
package io.element.android.libraries.androidutils.ui package io.element.android.libraries.androidutils.ui
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.ResultReceiver
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.WindowInsets import android.view.WindowInsets
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlin.coroutines.resume import kotlin.coroutines.resume
fun View.hideKeyboard() { fun View.hideKeyboard() {
@@ -22,6 +25,32 @@ fun View.hideKeyboard() {
imm?.hideSoftInputFromWindow(windowToken, 0) imm?.hideSoftInputFromWindow(windowToken, 0)
} }
suspend fun View.hideKeyboardAndAwaitAnimation() {
val imm = context?.getSystemService<InputMethodManager>()
val mutex = Mutex()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setOnApplyWindowInsetsListener { view, insets ->
if (!insets.isVisible(WindowInsets.Type.ime())) {
mutex.unlock()
}
insets
}
imm?.hideSoftInputFromWindow(windowToken, 0)
} else {
@Suppress("DEPRECATION")
imm?.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN ||
resultCode == InputMethodManager.RESULT_HIDDEN) {
mutex.unlock()
}
}
})
}
mutex.lock()
}
fun View.showKeyboard(andRequestFocus: Boolean = false) { fun View.showKeyboard(andRequestFocus: Boolean = false) {
if (andRequestFocus) { if (andRequestFocus) {
requestFocus() requestFocus()

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -54,14 +55,14 @@ fun CreateDmConfirmationBottomSheet(
ModalBottomSheet( ModalBottomSheet(
modifier = modifier, modifier = modifier,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Spacer(modifier = Modifier.height(24.dp))
Avatar( Avatar(
avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation),
avatarType = AvatarType.User, avatarType = AvatarType.User,
@@ -93,7 +94,6 @@ fun CreateDmConfirmationBottomSheet(
onClick = onDismiss, onClick = onDismiss,
text = stringResource(CommonStrings.action_cancel), text = stringResource(CommonStrings.action_cancel),
) )
Spacer(modifier = Modifier.height(16.dp))
} }
} }
} }