feat: Add GIF keyboard support to short note post screen

Enable the message field to receive images/GIFs from Android's keyboard
using Compose Foundation's contentReceiver API. When content is received,
it's routed through the existing selectImage flow for upload.

https://claude.ai/code/session_019GfY3MtCPcYn3wqTHCMSNu
This commit is contained in:
Claude
2026-03-30 20:33:55 +00:00
parent 4b9c3f239c
commit 95d678f62f
3 changed files with 51 additions and 2 deletions
@@ -20,6 +20,14 @@
*/
package com.vitorpamplona.amethyst.ui.components
import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.MediaType
import androidx.compose.foundation.content.ReceiveContentListener
import androidx.compose.foundation.content.TransferableContent
import androidx.compose.foundation.content.consume
import androidx.compose.foundation.content.contentReceiver
import androidx.compose.foundation.content.hasMediaType
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.PaddingValues
@@ -58,12 +66,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
// The only change is the contentPadding below
// New TextFieldState-based overload for GIF keyboard support
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ThinPaddingTextField(
state: TextFieldState,
modifier: Modifier = Modifier,
onTextChanged: (() -> Unit)? = null,
onContentReceived: ((Uri, String?) -> Unit)? = null,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
@@ -131,11 +140,41 @@ fun ThinPaddingTextField(
TextFieldLineLimits.MultiLine(minLines, maxLines)
}
val contentModifier =
if (onContentReceived != null) {
modifier.contentReceiver(
object : ReceiveContentListener {
override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
if (!transferableContent.hasMediaType(MediaType.Image)) {
return transferableContent
}
val remaining =
transferableContent.consume { item ->
val uri = item.uri
if (uri != null) {
onContentReceived(
uri,
transferableContent.clipEntry.clipData.description
.getMimeType(0),
)
true
} else {
false
}
}
return remaining
}
},
)
} else {
modifier
}
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
state = state,
modifier =
modifier
contentModifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = 36.dp,
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
import android.net.Uri
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
@@ -51,6 +52,7 @@ fun MessageField(
placeholder: Int,
viewModel: IMessageField,
requestFocus: Boolean = true,
onContentReceived: ((Uri, String?) -> Unit)? = null,
) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
@@ -67,6 +69,7 @@ fun MessageField(
ThinPaddingTextField(
state = viewModel.message,
onTextChanged = viewModel::onMessageChanged,
onContentReceived = onContentReceived,
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
@@ -323,6 +323,13 @@ private fun NewPostScreenBody(
MessageField(
R.string.what_s_on_your_mind,
postViewModel,
onContentReceived = { uri, mimeType ->
postViewModel.selectImage(
persistentListOf(
SelectedMedia(uri, mimeType),
),
)
},
)
}
}