feat: do not attempt to fit panes to keyboard height when the keyboard is unreasonably short (phys kb, floating)

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-12-25 04:08:14 +01:00
parent c69ffc3a7d
commit c1053f0702
2 changed files with 233 additions and 125 deletions

View File

@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imeAnimationTarget
@ -50,6 +51,8 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.AssistChip
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -162,6 +165,8 @@ private fun pxAsDp(px: Int): Dp {
).dp
}
private const val NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD = 500
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ChannelScreen(
@ -206,6 +211,12 @@ fun ChannelScreen(
label = "keyboardHeight"
)
val notEnoughSpaceForPanes by remember {
derivedStateOf {
viewModel.keyboardHeight < NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD
}
}
LaunchedEffect(imeTarget) {
if (imeTarget > 0) {
viewModel.updateSaveKeyboardHeight(imeTarget)
@ -279,6 +290,70 @@ fun ChannelScreen(
}
}
}
val openCameraCallback = cb@{
// Create a new content URI to store the captured image.
val contentResolver =
context.contentResolver
val contentValues = ContentValues().apply {
put(
MediaStore.MediaColumns.DISPLAY_NAME,
"RVL_${System.currentTimeMillis()}.jpg"
)
put(
MediaStore.MediaColumns.MIME_TYPE,
"image/jpeg"
)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
}
try {
capturedPhotoUri.value =
contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
} catch (e: Exception) {
Toast.makeText(
context,
context.getString(
R.string.file_picker_chip_camera_failed
),
Toast.LENGTH_SHORT
).show()
return@cb
}
try {
capturedPhotoUri.value?.let { uri ->
pickCameraLauncher.launch(uri)
}
} catch (e: Exception) {
Toast.makeText(
context,
context.getString(
R.string.file_picker_chip_camera_none_installed
),
Toast.LENGTH_SHORT
).show()
}
}
val openDocumentPickerCallback = {
pickFileLauncher.launch(arrayOf("*/*"))
}
val openPhotoPickerCallback = {
pickMediaLauncher.launch(
PickVisualMediaRequest(
mediaType = ActivityResultContracts.PickVisualMedia.ImageAndVideo
)
)
}
// </editor-fold>
// <editor-fold desc="UI elements">
val lazyListState = rememberLazyListState()
@ -869,6 +944,53 @@ fun ChannelScreen(
channelId = channelId,
failedValidation = viewModel.draftContent.length > 2000,
)
DropdownMenu(
expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes,
onDismissRequest = {
viewModel.activePane = ChannelScreenActivePane.None
}
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_paperclip_24dp),
contentDescription = null // Provided by text below
)
},
text = { Text(stringResource(R.string.file_picker_chip_documents)) },
onClick = {
openDocumentPickerCallback()
viewModel.activePane = ChannelScreenActivePane.None
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_camera_24dp),
contentDescription = null // Provided by text below
)
},
text = { Text(stringResource(R.string.file_picker_chip_camera)) },
onClick = {
openCameraCallback()
viewModel.activePane = ChannelScreenActivePane.None
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_image_multiple_24dp),
contentDescription = null // Provided by text below
)
},
text = { Text(stringResource(R.string.file_picker_chip_photo_picker)) },
onClick = {
openPhotoPickerCallback()
viewModel.activePane = ChannelScreenActivePane.None
}
)
}
}
} else {
Box(
@ -893,137 +1015,126 @@ fun ChannelScreen(
.background(MaterialTheme.colorScheme.surfaceContainer)
)
} else {
Box(
Modifier
.heightIn(min = pxAsDp(fallbackKeyboardHeight))
) {
if (!notEnoughSpaceForPanes) {
Box(
Modifier.then(
if (emojiSearchFocused) {
Modifier.requiredHeight(
pxAsDp(
max(
imeCurrentInset * 2,
Modifier
.heightIn(min = pxAsDp(fallbackKeyboardHeight))
) {
Box(
Modifier.then(
if (emojiSearchFocused) {
Modifier.requiredHeight(
pxAsDp(
max(
imeCurrentInset * 2,
fallbackKeyboardHeight
)
)
)
} else {
Modifier.requiredHeight(
pxAsDp(
fallbackKeyboardHeight
)
)
)
} else {
Modifier.requiredHeight(pxAsDp(fallbackKeyboardHeight))
}
)
) {
when (viewModel.activePane) {
ChannelScreenActivePane.EmojiPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) {
viewModel.activePane = ChannelScreenActivePane.None
}
)
) {
when (viewModel.activePane) {
ChannelScreenActivePane.EmojiPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) {
viewModel.activePane =
ChannelScreenActivePane.None
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(4.dp)
.navigationBarsPadding()
) {
EmojiPicker(
onEmojiSelected = viewModel::putAtCursorPosition,
bottomInset = pxAsDp(
max(
imeCurrentInset - navigationBarsInset,
0
)
),
onSearchFocus = {
emojiSearchFocused = it
}
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(4.dp)
.navigationBarsPadding()
) {
EmojiPicker(
onEmojiSelected = viewModel::putAtCursorPosition,
bottomInset = pxAsDp(
max(
imeCurrentInset - navigationBarsInset,
0
)
),
onSearchFocus = {
emojiSearchFocused = it
}
ChannelScreenActivePane.AttachmentPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
viewModel.activePane =
ChannelScreenActivePane.None
}
MediaPickerGateway(
onOpenPhotoPicker = {
openPhotoPickerCallback()
viewModel.activePane =
ChannelScreenActivePane.None
},
onOpenDocumentPicker = {
openDocumentPickerCallback()
viewModel.activePane =
ChannelScreenActivePane.None
},
onOpenCamera = {
openCameraCallback()
viewModel.activePane =
ChannelScreenActivePane.None
},
)
}
}
ChannelScreenActivePane.AttachmentPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
viewModel.activePane = ChannelScreenActivePane.None
else -> {
// Do nothing
}
MediaPickerGateway(
onOpenPhotoPicker = {
pickMediaLauncher.launch(
PickVisualMediaRequest(
mediaType = ActivityResultContracts.PickVisualMedia.ImageAndVideo
)
)
viewModel.activePane =
ChannelScreenActivePane.None
},
onOpenDocumentPicker = {
pickFileLauncher.launch(arrayOf("*/*"))
viewModel.activePane =
ChannelScreenActivePane.None
},
onOpenCamera = {
// Create a new content URI to store the captured image.
val contentResolver =
context.contentResolver
val contentValues = ContentValues().apply {
put(
MediaStore.MediaColumns.DISPLAY_NAME,
"RVL_${System.currentTimeMillis()}.jpg"
)
put(
MediaStore.MediaColumns.MIME_TYPE,
"image/jpeg"
)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
}
try {
capturedPhotoUri.value =
contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
} catch (e: Exception) {
Toast.makeText(
context,
context.getString(
R.string.file_picker_chip_camera_failed
),
Toast.LENGTH_SHORT
).show()
return@MediaPickerGateway
}
try {
capturedPhotoUri.value?.let { uri ->
pickCameraLauncher.launch(uri)
}
} catch (e: Exception) {
Toast.makeText(
context,
context.getString(
R.string.file_picker_chip_camera_none_installed
),
Toast.LENGTH_SHORT
).show()
}
viewModel.activePane =
ChannelScreenActivePane.None
},
)
}
else -> {
// Do nothing
}
}
Box(Modifier.imePadding())
}
Box(Modifier.imePadding())
} else {
if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) {
viewModel.activePane =
ChannelScreenActivePane.None
}
Column(
modifier = Modifier
.fillMaxWidth()
.height(600.dp)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(4.dp)
.navigationBarsPadding()
) {
EmojiPicker(
onEmojiSelected = viewModel::putAtCursorPosition,
bottomInset = pxAsDp(
max(
imeCurrentInset - navigationBarsInset,
0
)
),
onSearchFocus = {
emojiSearchFocused = it
}
)
}
}
Box(
Modifier
.imePadding()
.navigationBarsPadding()
)
}
}
}

View File

@ -541,13 +541,6 @@
<string name="colour_picker_cancel">Cancel</string>
<string name="colour_picker_apply">Apply</string>
<string name="file_picker_cannot_attach_file_invalid">This file is invalid and cannot be attached.</string>
<string name="file_picker_permission_request_header">We need your permission to access photos and videos</string>
<string name="file_picker_permission_request_body">You will be able to attach photos and videos to your messages afterwards.</string>
<string name="file_picker_permission_request_cta">Allow access</string>
<string name="file_picker_permission_unpartialise_request_header">You have granted partial access to photos and videos</string>
<string name="file_picker_permission_unpartialise_request_body">To attach all your photos and videos, we need your permission</string>
<string name="file_picker_permission_unpartialise_request_cta">Allow full access</string>
<string name="file_picker_chip_photo_picker">Select Photos</string>
<string name="file_picker_chip_documents">Files</string>
<string name="file_picker_chip_camera">Camera</string>
@ -581,6 +574,10 @@
<string name="spark_notifications_rationale_cta">Enable notifications</string>
<string name="spark_notifications_rationale_dismiss">Not now</string>
<string name="spark_keyboard_shortcuts">Using a keyboard?</string>
<string name="spark_keyboard_shortcuts_description">Revolt has keyboard shortcuts! Press %1$s to see them.</string>
<string name="spark_keyboard_shortcuts_cta">Open shortcuts</string>
<string name="notice_platform_mod_dm_title">Important notice regarding your account</string>
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
<string name="notice_platform_mod_dm_acknowledge">View</string>