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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imeAnimationTarget 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.material.icons.filled.Menu
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -162,6 +165,8 @@ private fun pxAsDp(px: Int): Dp {
).dp ).dp
} }
private const val NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD = 500
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun ChannelScreen( fun ChannelScreen(
@ -206,6 +211,12 @@ fun ChannelScreen(
label = "keyboardHeight" label = "keyboardHeight"
) )
val notEnoughSpaceForPanes by remember {
derivedStateOf {
viewModel.keyboardHeight < NOT_ENOUGH_SPACE_FOR_PANES_THRESHOLD
}
}
LaunchedEffect(imeTarget) { LaunchedEffect(imeTarget) {
if (imeTarget > 0) { if (imeTarget > 0) {
viewModel.updateSaveKeyboardHeight(imeTarget) 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>
// <editor-fold desc="UI elements"> // <editor-fold desc="UI elements">
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -869,6 +944,53 @@ fun ChannelScreen(
channelId = channelId, channelId = channelId,
failedValidation = viewModel.draftContent.length > 2000, 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 { } else {
Box( Box(
@ -893,6 +1015,7 @@ fun ChannelScreen(
.background(MaterialTheme.colorScheme.surfaceContainer) .background(MaterialTheme.colorScheme.surfaceContainer)
) )
} else { } else {
if (!notEnoughSpaceForPanes) {
Box( Box(
Modifier Modifier
.heightIn(min = pxAsDp(fallbackKeyboardHeight)) .heightIn(min = pxAsDp(fallbackKeyboardHeight))
@ -909,14 +1032,19 @@ fun ChannelScreen(
) )
) )
} else { } else {
Modifier.requiredHeight(pxAsDp(fallbackKeyboardHeight)) Modifier.requiredHeight(
pxAsDp(
fallbackKeyboardHeight
)
)
} }
) )
) { ) {
when (viewModel.activePane) { when (viewModel.activePane) {
ChannelScreenActivePane.EmojiPicker -> { ChannelScreenActivePane.EmojiPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) {
viewModel.activePane = ChannelScreenActivePane.None viewModel.activePane =
ChannelScreenActivePane.None
} }
Column( Column(
@ -943,75 +1071,23 @@ fun ChannelScreen(
ChannelScreenActivePane.AttachmentPicker -> { ChannelScreenActivePane.AttachmentPicker -> {
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
viewModel.activePane = ChannelScreenActivePane.None viewModel.activePane =
ChannelScreenActivePane.None
} }
MediaPickerGateway( MediaPickerGateway(
onOpenPhotoPicker = { onOpenPhotoPicker = {
pickMediaLauncher.launch( openPhotoPickerCallback()
PickVisualMediaRequest(
mediaType = ActivityResultContracts.PickVisualMedia.ImageAndVideo
)
)
viewModel.activePane = viewModel.activePane =
ChannelScreenActivePane.None ChannelScreenActivePane.None
}, },
onOpenDocumentPicker = { onOpenDocumentPicker = {
pickFileLauncher.launch(arrayOf("*/*")) openDocumentPickerCallback()
viewModel.activePane = viewModel.activePane =
ChannelScreenActivePane.None ChannelScreenActivePane.None
}, },
onOpenCamera = { onOpenCamera = {
// Create a new content URI to store the captured image. openCameraCallback()
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 = viewModel.activePane =
ChannelScreenActivePane.None ChannelScreenActivePane.None
}, },
@ -1025,6 +1101,41 @@ fun ChannelScreen(
} }
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_cancel">Cancel</string>
<string name="colour_picker_apply">Apply</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_photo_picker">Select Photos</string>
<string name="file_picker_chip_documents">Files</string> <string name="file_picker_chip_documents">Files</string>
<string name="file_picker_chip_camera">Camera</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_cta">Enable notifications</string>
<string name="spark_notifications_rationale_dismiss">Not now</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_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_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> <string name="notice_platform_mod_dm_acknowledge">View</string>