feat: replace message field with view-backed variant

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-22 02:07:54 +02:00
parent dda4ec0631
commit 0c90c8c0e6
3 changed files with 268 additions and 252 deletions

View File

@ -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"
)
}

View File

@ -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 = {}
)
}

View File

@ -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 = {