diff --git a/app/src/main/java/chat/revolt/components/chat/MessageField.kt b/app/src/main/java/chat/revolt/components/chat/MessageField.kt deleted file mode 100644 index dd923edc..00000000 --- a/app/src/main/java/chat/revolt/components/chat/MessageField.kt +++ /dev/null @@ -1,245 +0,0 @@ -package chat.revolt.components.chat - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandIn -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import chat.revolt.R -import chat.revolt.activities.RevoltTweenFloat -import chat.revolt.activities.RevoltTweenInt -import chat.revolt.api.schemas.ChannelType - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MessageField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - onAddAttachment: () -> Unit, - onPickEmoji: () -> Unit, - onSendMessage: () -> Unit, - channelType: ChannelType, - channelName: String, - modifier: Modifier = Modifier, - forceSendButton: Boolean = false, - disabled: Boolean = false, - editMode: Boolean = false, - cancelEdit: () -> Unit = {}, - onFocusChange: (Boolean) -> Unit = {} -) { - val focusRequester = remember { FocusRequester() } - val placeholderResource = when (channelType) { - ChannelType.DirectMessage -> R.string.message_field_placeholder_dm - ChannelType.Group -> R.string.message_field_placeholder_group - ChannelType.TextChannel -> R.string.message_field_placeholder_text - ChannelType.VoiceChannel -> R.string.message_field_placeholder_voice - ChannelType.SavedMessages -> R.string.message_field_placeholder_notes - } - - val sendButtonVisible = (value.text.isNotBlank() || forceSendButton) && !disabled - - LaunchedEffect(editMode) { - if (editMode) { - focusRequester.requestFocus() - } else { - focusRequester.freeFocus() - } - } - - Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) - ) { - BasicTextField( - value = value, - onValueChange = onValueChange, - singleLine = false, - enabled = !disabled, - textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface), - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - modifier = modifier - .weight(1f) - .focusRequester(focusRequester) - .onFocusChanged { state -> - onFocusChange(state.isFocused) - }, - keyboardOptions = KeyboardOptions.Default, - keyboardActions = KeyboardActions.Default, - decorationBox = @Composable { innerTextField -> - TextFieldDefaults.DecorationBox( - value = value.text, - innerTextField = innerTextField, - enabled = !disabled, - singleLine = false, - visualTransformation = VisualTransformation.None, - interactionSource = remember { MutableInteractionSource() }, - placeholder = { - Text( - text = stringResource( - id = placeholderResource, - channelName - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - colors = TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - unfocusedPlaceholderColor = Color.Gray, - focusedPlaceholderColor = Color.Gray, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( - 1.dp - ), - focusedContainerColor = Color.Transparent - ), - contentPadding = PaddingValues(16.dp), - leadingIcon = { - Icon( - when { - editMode -> Icons.Default.Close - else -> Icons.Default.Add - }, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - contentDescription = stringResource(id = R.string.add_attachment_alt), - modifier = Modifier - .clip(CircleShape) - .size(32.dp) - .clickable { - when { - editMode -> cancelEdit() - else -> { - focusRequester.freeFocus() // hide keyboard because it's annoying - onAddAttachment() - } - } - } - .padding(4.dp) - .testTag("add_attachment") - ) - }, - trailingIcon = { - Row { - Icon( - painter = painterResource(R.drawable.ic_emoticon_24dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - contentDescription = stringResource(id = R.string.pick_emoji_alt), - modifier = Modifier - .clip(CircleShape) - .size(32.dp) - .clickable { - focusRequester.freeFocus() // hide keyboard because it's annoying - onPickEmoji() - } - .padding(4.dp) - .testTag("pick_emoji") - ) - - Spacer(modifier = Modifier.width(8.dp)) - - AnimatedVisibility( - sendButtonVisible, - enter = expandIn(initialSize = { full -> - IntSize( - 0, - full.height - ) - }) + slideInHorizontally( - animationSpec = RevoltTweenInt, - initialOffsetX = { -it } - ) + fadeIn(animationSpec = RevoltTweenFloat), - exit = shrinkOut(targetSize = { full -> - IntSize( - 0, - full.height - ) - }) + slideOutHorizontally( - animationSpec = RevoltTweenInt, - targetOffsetX = { it } - ) + fadeOut(animationSpec = RevoltTweenFloat) - ) { - Icon( - when { - editMode -> Icons.Default.Edit - else -> Icons.Default.Send - }, - tint = MaterialTheme.colorScheme.primary, - contentDescription = stringResource(id = R.string.send_alt), - modifier = Modifier - .padding(end = 8.dp) - .clip(CircleShape) - .clickable { onSendMessage() } - .size(32.dp) - .padding(4.dp) - .testTag("send_message") - ) - } - } - } - ) - } - ) - } -} - -@Preview -@Composable -fun MessageFieldPreview() { - MessageField( - value = TextFieldValue("Hello world!"), - onValueChange = {}, - onSendMessage = {}, - onAddAttachment = {}, - onPickEmoji = {}, - channelType = ChannelType.TextChannel, - channelName = "general" - ) -} diff --git a/app/src/main/java/chat/revolt/components/chat/NativeMessageField.kt b/app/src/main/java/chat/revolt/components/chat/NativeMessageField.kt new file mode 100644 index 00000000..cc748a8f --- /dev/null +++ b/app/src/main/java/chat/revolt/components/chat/NativeMessageField.kt @@ -0,0 +1,261 @@ +package chat.revolt.components.chat + +import android.content.Context +import android.os.Build +import android.view.inputmethod.InputMethodManager +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.setPadding +import androidx.core.widget.addTextChangedListener +import chat.revolt.R +import chat.revolt.activities.RevoltTweenFloat +import chat.revolt.activities.RevoltTweenInt +import chat.revolt.api.schemas.ChannelType + + +@Composable +fun NativeMessageField( + value: String, + onValueChange: (String) -> Unit, + onAddAttachment: () -> Unit, + onPickEmoji: () -> Unit, + onSendMessage: () -> Unit, + channelType: ChannelType, + channelName: String, + modifier: Modifier = Modifier, + forceSendButton: Boolean = false, + disabled: Boolean = false, + editMode: Boolean = false, + cancelEdit: () -> Unit = {}, + onFocusChange: (Boolean) -> Unit = {} +) { + val placeholderResource = when (channelType) { + ChannelType.DirectMessage -> R.string.message_field_placeholder_dm + ChannelType.Group -> R.string.message_field_placeholder_group + ChannelType.TextChannel -> R.string.message_field_placeholder_text + ChannelType.VoiceChannel -> R.string.message_field_placeholder_voice + ChannelType.SavedMessages -> R.string.message_field_placeholder_notes + } + + var requestFocus by remember { mutableStateOf({}) } + var clearFocus by remember { mutableStateOf({}) } + + val sendButtonVisible = (value.isNotBlank() || forceSendButton) && !disabled + + val density = LocalDensity.current + + val selectionColour = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f).toArgb() + val contentColour = LocalContentColor.current.toArgb() + + LaunchedEffect(editMode) { + if (editMode) { + requestFocus() + } else { + clearFocus() + } + } + + Row( + modifier = modifier + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + when { + editMode -> Icons.Default.Close + else -> Icons.Default.Add + }, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + contentDescription = stringResource(id = R.string.add_attachment_alt), + modifier = Modifier + .clip(CircleShape) + .size(32.dp) + .clickable { + when { + editMode -> cancelEdit() + else -> { + // hide keyboard because it's annoying + clearFocus() + onAddAttachment() + } + } + } + .padding(4.dp) + .testTag("add_attachment") + ) + + AndroidView( + factory = { context -> + com.google.android.material.textfield.TextInputEditText(context).apply { + background = null + textSize = 16f + setPadding((density.density * 16.dp.value).toInt()) + addTextChangedListener { + onValueChange(it.toString()) + } + onFocusChangeListener = android.view.View.OnFocusChangeListener { _, hasFocus -> + val keyboard = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + if (hasFocus) { + keyboard.showSoftInput( + this, + 0 + ) + } else { + keyboard.hideSoftInputFromWindow(this.getWindowToken(), 0) + } + + onFocusChange(hasFocus) + } + isFocusable = true + isFocusableInTouchMode = true + + typeface = ResourcesCompat.getFont(context, R.font.inter) + + highlightColor = selectionColour + setTextColor(contentColour) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setTextCursorDrawable(null) + } + + this.alpha = 1f + + clearFocus = { + this.clearFocus() + } + requestFocus = { + this.requestFocus() + } + } + }, + update = { + if (value != it.text.toString()) { + it.setText(value) + it.setSelection(value.length) + } + it.hint = it.context.getString(placeholderResource, channelName) + }, + modifier = Modifier + .weight(1f) + .testTag("message_field") + ) + + Icon( + painter = painterResource(R.drawable.ic_emoticon_24dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + contentDescription = stringResource(id = R.string.pick_emoji_alt), + modifier = Modifier + .clip(CircleShape) + .size(32.dp) + .clickable { + clearFocus() + onPickEmoji() + } + .padding(4.dp) + .testTag("pick_emoji") + ) + + Spacer(modifier = Modifier.width(8.dp)) + + AnimatedVisibility( + sendButtonVisible, + enter = expandIn(initialSize = { full -> + IntSize( + 0, + full.height + ) + }) + slideInHorizontally( + animationSpec = RevoltTweenInt, + initialOffsetX = { -it } + ) + fadeIn(animationSpec = RevoltTweenFloat), + exit = shrinkOut(targetSize = { full -> + IntSize( + 0, + full.height + ) + }) + slideOutHorizontally( + animationSpec = RevoltTweenInt, + targetOffsetX = { it } + ) + fadeOut(animationSpec = RevoltTweenFloat) + ) { + Icon( + when { + editMode -> Icons.Default.Edit + else -> Icons.Default.Send + }, + tint = MaterialTheme.colorScheme.primary, + contentDescription = stringResource(id = R.string.send_alt), + modifier = Modifier + .padding(end = 8.dp) + .clip(CircleShape) + .clickable { onSendMessage() } + .size(32.dp) + .padding(4.dp) + .testTag("send_message") + ) + } + } +} + +@Preview +@Composable +fun NativeMessageFieldPreview() { + NativeMessageField( + value = "Hello world!", + onValueChange = {}, + onAddAttachment = {}, + onPickEmoji = {}, + onSendMessage = {}, + channelType = ChannelType.DirectMessage, + channelName = "Test", + modifier = Modifier, + forceSendButton = false, + disabled = false, + editMode = false, + cancelEdit = {}, + onFocusChange = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 5d1a2797..92d1cf8b 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -73,7 +73,7 @@ import chat.revolt.api.routes.microservices.autumn.FileArgs import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.ChannelType import chat.revolt.components.chat.Message -import chat.revolt.components.chat.MessageField +import chat.revolt.components.chat.NativeMessageField import chat.revolt.components.chat.SystemMessage import chat.revolt.components.emoji.EmojiPicker import chat.revolt.components.media.InbuiltMediaPicker @@ -91,10 +91,10 @@ import chat.revolt.sheets.ChannelInfoSheet import chat.revolt.sheets.MessageContextSheet import com.discord.simpleast.core.simple.SimpleMarkdownRules import com.discord.simpleast.core.simple.SimpleRenderer -import java.io.File -import java.io.FileNotFoundException import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import java.io.File +import java.io.FileNotFoundException @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -478,11 +478,11 @@ fun ChannelScreen( textAlign = TextAlign.Center ) } else { - MessageField( - value = fieldContent, + NativeMessageField( + value = fieldContent.text, onValueChange = { - viewModel.pendingMessageContent = it.text - viewModel.textSelection = it.selection + viewModel.pendingMessageContent = it + // viewModel.textSelection = it.selection }, onSendMessage = viewModel::sendPendingMessage, onAddAttachment = {