feat: emoji autocomplete
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
4e8b3746ac
commit
2d911f6e86
|
|
@ -9,7 +9,7 @@ object MessageProcessor {
|
|||
private val ChannelRegex = Regex("(?:\\s|^)#(.+?)(?:\\s|\$)", RegexOption.IGNORE_CASE)
|
||||
private val EmojiRegex = Regex(":(.+?):", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val emojiMetadata = EmojiImpl()
|
||||
val emoji = EmojiImpl()
|
||||
|
||||
/**
|
||||
* Processes an outgoing message for sending.
|
||||
|
|
@ -52,7 +52,7 @@ object MessageProcessor {
|
|||
val emojiName = EmojiRegex.matchEntire(emoji)?.destructured?.component1()
|
||||
?: return@fold acc
|
||||
|
||||
val byShortcode = emojiMetadata.unicodeByShortcode(emojiName)
|
||||
val byShortcode = this.emoji.unicodeByShortcode(emojiName)
|
||||
?: return@fold acc
|
||||
|
||||
acc.replace(":$emojiName:", byShortcode)
|
||||
|
|
|
|||
|
|
@ -16,14 +16,22 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
|
|
@ -33,12 +41,17 @@ 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.SuggestionChip
|
||||
import androidx.compose.material3.SuggestionChipDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -62,7 +75,70 @@ import chat.revolt.R
|
|||
import chat.revolt.activities.RevoltTweenFloat
|
||||
import chat.revolt.activities.RevoltTweenInt
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.api.schemas.Member
|
||||
import chat.revolt.internals.Autocomplete
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun String.applyAutocompleteSuggestion(
|
||||
suggestion: AutocompleteSuggestion,
|
||||
cursorPosition: Int
|
||||
): String {
|
||||
return when (suggestion) {
|
||||
is AutocompleteSuggestion.User -> {
|
||||
this.replaceRange(
|
||||
cursorPosition - suggestion.query.length - 1,
|
||||
cursorPosition,
|
||||
"@${suggestion.user.username}#${suggestion.user.discriminator} "
|
||||
)
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Channel -> {
|
||||
if (suggestion.channel.name?.contains(" ") == true) {
|
||||
this.replaceRange(
|
||||
cursorPosition - suggestion.query.length - 1,
|
||||
cursorPosition,
|
||||
"#${suggestion.channel.name} "
|
||||
)
|
||||
} else {
|
||||
this.replaceRange(
|
||||
cursorPosition - suggestion.query.length - 1,
|
||||
cursorPosition,
|
||||
"<#${suggestion.channel.id}> "
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Emoji -> {
|
||||
this.replaceRange(
|
||||
cursorPosition - suggestion.query.length - 1,
|
||||
cursorPosition,
|
||||
suggestion.shortcode + " "
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AutocompleteSuggestion {
|
||||
data class User(
|
||||
val user: chat.revolt.api.schemas.User,
|
||||
val member: Member?,
|
||||
val query: String
|
||||
) : AutocompleteSuggestion()
|
||||
|
||||
data class Channel(
|
||||
val channel: chat.revolt.api.schemas.Channel,
|
||||
val query: String
|
||||
) : AutocompleteSuggestion()
|
||||
|
||||
data class Emoji(
|
||||
val shortcode: String,
|
||||
val unicode: String?,
|
||||
val custom: chat.revolt.api.schemas.Emoji?,
|
||||
val query: String
|
||||
) : AutocompleteSuggestion()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NativeMessageField(
|
||||
value: String,
|
||||
|
|
@ -77,6 +153,8 @@ fun NativeMessageField(
|
|||
forceSendButton: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
editMode: Boolean = false,
|
||||
serverId: String? = null,
|
||||
channelId: String? = null,
|
||||
cancelEdit: () -> Unit = {},
|
||||
onFocusChange: (Boolean) -> Unit = {},
|
||||
onSelectionChange: (Pair<Int, Int>) -> Unit = {}
|
||||
|
|
@ -96,11 +174,17 @@ fun NativeMessageField(
|
|||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val selectionColour = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f).toArgb()
|
||||
val cursorColour = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val contentColour = LocalContentColor.current.toArgb()
|
||||
val placeholderColour = LocalContentColor.current.copy(alpha = 0.5f).toArgb()
|
||||
|
||||
var selection by remember { mutableStateOf(0 to 0) }
|
||||
val autocompleteSuggestions = remember { mutableStateListOf<AutocompleteSuggestion>() }
|
||||
val autocompleteSuggestionScrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(editMode) {
|
||||
if (editMode) {
|
||||
requestFocus()
|
||||
|
|
@ -109,201 +193,350 @@ fun NativeMessageField(
|
|||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Column(
|
||||
modifier = modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||
) {
|
||||
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 ->
|
||||
object : androidx.appcompat.widget.AppCompatEditText(context) {
|
||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||
var ic = super.onCreateInputConnection(outAttrs)
|
||||
EditorInfoCompat.setContentMimeTypes(
|
||||
outAttrs,
|
||||
arrayOf("image/*")
|
||||
)
|
||||
val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this)
|
||||
if (mimeTypes != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes)
|
||||
ic = ic?.let { InputConnectionCompat.createWrapper(this, it, outAttrs) }
|
||||
}
|
||||
return ic
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
onSelectionChange(selStart to selEnd)
|
||||
}
|
||||
}.apply {
|
||||
background = null
|
||||
textSize = 16f
|
||||
setPadding((density.density * 16.dp.value).toInt())
|
||||
|
||||
// Propagate text changes to parent
|
||||
addTextChangedListener {
|
||||
onValueChange(it.toString())
|
||||
}
|
||||
|
||||
// Hide/show keyboard on focus change and propagate to parent
|
||||
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.windowToken, 0)
|
||||
}
|
||||
|
||||
onFocusChange(hasFocus)
|
||||
}
|
||||
|
||||
ViewCompat.setOnReceiveContentListener(
|
||||
this,
|
||||
arrayOf("image/*")
|
||||
) { _, payload ->
|
||||
// Check mimetype
|
||||
if (payload.clip.description.hasMimeType("image/*")) {
|
||||
// Get image
|
||||
val item = payload.clip.getItemAt(0)
|
||||
val uri = item.uri
|
||||
|
||||
if (uri == null) {
|
||||
Log.e("MessageField", "Received payload with null uri")
|
||||
return@setOnReceiveContentListener payload
|
||||
}
|
||||
|
||||
onCommitAttachment(uri)
|
||||
|
||||
return@setOnReceiveContentListener null
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
|
||||
typeface = ResourcesCompat.getFont(context, R.font.inter)
|
||||
|
||||
// Set colours
|
||||
highlightColor = selectionColour
|
||||
setTextColor(contentColour)
|
||||
setHintTextColor(ColorStateList.valueOf(placeholderColour))
|
||||
|
||||
// Caret colour and size
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val shapeDrawable = ShapeDrawable(RectShape())
|
||||
shapeDrawable.paint.color = cursorColour
|
||||
val sizeInDp = 1
|
||||
val sizeInPixels = (sizeInDp * resources.displayMetrics.density).toInt()
|
||||
shapeDrawable.intrinsicWidth = sizeInPixels
|
||||
shapeDrawable.intrinsicHeight = sizeInPixels
|
||||
|
||||
setTextCursorDrawable(shapeDrawable)
|
||||
}
|
||||
|
||||
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,
|
||||
visible = autocompleteSuggestions.size > 0,
|
||||
enter = expandIn(initialSize = { full ->
|
||||
IntSize(
|
||||
0,
|
||||
full.height
|
||||
full.width,
|
||||
0
|
||||
)
|
||||
}) + slideInHorizontally(
|
||||
}) + slideInVertically(
|
||||
animationSpec = RevoltTweenInt,
|
||||
initialOffsetX = { -it }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat),
|
||||
initialOffsetY = { -it }
|
||||
),
|
||||
exit = shrinkOut(targetSize = { full ->
|
||||
IntSize(
|
||||
0,
|
||||
full.height
|
||||
full.width,
|
||||
0
|
||||
)
|
||||
}) + slideOutHorizontally(
|
||||
}) + slideOutVertically(
|
||||
animationSpec = RevoltTweenInt,
|
||||
targetOffsetX = { it }
|
||||
) + fadeOut(animationSpec = RevoltTweenFloat)
|
||||
targetOffsetY = { it }
|
||||
)
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(autocompleteSuggestions.size, key = {
|
||||
when (val item = autocompleteSuggestions[it]) {
|
||||
is AutocompleteSuggestion.User -> item.user.id!!
|
||||
is AutocompleteSuggestion.Channel -> item.channel.id!!
|
||||
is AutocompleteSuggestion.Emoji -> item.shortcode
|
||||
}
|
||||
}) {
|
||||
when (val item = autocompleteSuggestions[it]) {
|
||||
is AutocompleteSuggestion.User -> {
|
||||
SuggestionChip(
|
||||
onClick = {
|
||||
onValueChange(
|
||||
value.applyAutocompleteSuggestion(
|
||||
item,
|
||||
selection.first
|
||||
)
|
||||
)
|
||||
},
|
||||
label = { Text("@${item.user.username}#${item.user.discriminator}") },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_human_greeting_variant_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SuggestionChipDefaults.IconSize)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Channel -> {
|
||||
SuggestionChip(
|
||||
onClick = {
|
||||
onValueChange(
|
||||
value.applyAutocompleteSuggestion(
|
||||
item,
|
||||
selection.first
|
||||
)
|
||||
)
|
||||
},
|
||||
label = { Text("#${item.channel.name}") },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_pound_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SuggestionChipDefaults.IconSize)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Emoji -> {
|
||||
SuggestionChip(
|
||||
onClick = {
|
||||
onValueChange(
|
||||
value.applyAutocompleteSuggestion(
|
||||
item,
|
||||
selection.first
|
||||
)
|
||||
)
|
||||
},
|
||||
label = { Text(item.shortcode) },
|
||||
icon = {
|
||||
if (item.unicode != null) {
|
||||
Text(
|
||||
item.unicode,
|
||||
modifier = Modifier
|
||||
.size(SuggestionChipDefaults.IconSize)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_emoticon_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SuggestionChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Icon(
|
||||
when {
|
||||
editMode -> Icons.Default.Edit
|
||||
else -> Icons.Default.Send
|
||||
editMode -> Icons.Default.Close
|
||||
else -> Icons.Default.Add
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(id = R.string.send_alt),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable { onSendMessage() }
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
when {
|
||||
editMode -> cancelEdit()
|
||||
else -> {
|
||||
// hide keyboard because it's annoying
|
||||
clearFocus()
|
||||
onAddAttachment()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(4.dp)
|
||||
.testTag("send_message")
|
||||
.testTag("add_attachment")
|
||||
)
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
object : androidx.appcompat.widget.AppCompatEditText(context) {
|
||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||
var ic = super.onCreateInputConnection(outAttrs)
|
||||
EditorInfoCompat.setContentMimeTypes(
|
||||
outAttrs,
|
||||
arrayOf("image/*")
|
||||
)
|
||||
val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this)
|
||||
if (mimeTypes != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes)
|
||||
ic = ic?.let {
|
||||
InputConnectionCompat.createWrapper(
|
||||
this,
|
||||
it,
|
||||
outAttrs
|
||||
)
|
||||
}
|
||||
}
|
||||
return ic
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
onSelectionChange(selStart to selEnd)
|
||||
selection = selStart to selEnd
|
||||
|
||||
scope.launch {
|
||||
autocompleteSuggestionScrollState.scrollTo(0)
|
||||
}
|
||||
autocompleteSuggestions.clear()
|
||||
|
||||
if (text?.isNotBlank() == false) return
|
||||
if (selStart != selEnd) return
|
||||
|
||||
val lastWord =
|
||||
text?.substring(0, selStart)?.split(" ")?.lastOrNull() ?: return
|
||||
|
||||
when {
|
||||
lastWord.startsWith(':') && !lastWord.endsWith(':') -> {
|
||||
autocompleteSuggestions.addAll(
|
||||
Autocomplete.emoji(lastWord.substring(1))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
background = null
|
||||
textSize = 16f
|
||||
setPadding((density.density * 16.dp.value).toInt())
|
||||
|
||||
// Propagate text changes to parent
|
||||
addTextChangedListener {
|
||||
onValueChange(it.toString())
|
||||
}
|
||||
|
||||
// Hide/show keyboard on focus change and propagate to parent
|
||||
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.windowToken, 0)
|
||||
}
|
||||
|
||||
onFocusChange(hasFocus)
|
||||
}
|
||||
|
||||
ViewCompat.setOnReceiveContentListener(
|
||||
this,
|
||||
arrayOf("image/*")
|
||||
) { _, payload ->
|
||||
// Check mimetype
|
||||
if (payload.clip.description.hasMimeType("image/*")) {
|
||||
// Get image
|
||||
val item = payload.clip.getItemAt(0)
|
||||
val uri = item.uri
|
||||
|
||||
if (uri == null) {
|
||||
Log.e("MessageField", "Received payload with null uri")
|
||||
return@setOnReceiveContentListener payload
|
||||
}
|
||||
|
||||
onCommitAttachment(uri)
|
||||
|
||||
return@setOnReceiveContentListener null
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
|
||||
typeface = ResourcesCompat.getFont(context, R.font.inter)
|
||||
|
||||
// Set colours
|
||||
highlightColor = selectionColour
|
||||
setTextColor(contentColour)
|
||||
setHintTextColor(ColorStateList.valueOf(placeholderColour))
|
||||
|
||||
// Caret colour and size
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val shapeDrawable = ShapeDrawable(RectShape())
|
||||
shapeDrawable.paint.color = cursorColour
|
||||
val sizeInDp = 1
|
||||
val sizeInPixels = (sizeInDp * resources.displayMetrics.density).toInt()
|
||||
shapeDrawable.intrinsicWidth = sizeInPixels
|
||||
shapeDrawable.intrinsicHeight = sizeInPixels
|
||||
|
||||
setTextCursorDrawable(shapeDrawable)
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package chat.revolt.internals
|
||||
|
||||
import chat.revolt.components.chat.AutocompleteSuggestion
|
||||
|
||||
object Autocomplete {
|
||||
private val emojiImpl = EmojiImpl()
|
||||
|
||||
fun emoji(query: String): List<AutocompleteSuggestion.Emoji> {
|
||||
val unicodeResults = emojiImpl.shortcodeContains(query).map {
|
||||
AutocompleteSuggestion.Emoji(
|
||||
it.shortcodes.find { shortcode -> shortcode.contains(query) }
|
||||
?: it.shortcodes.first(),
|
||||
it.base.joinToString("") { s -> String(Character.toChars(s.toInt())) },
|
||||
null,
|
||||
query
|
||||
)
|
||||
}.distinctBy { it.shortcode }
|
||||
|
||||
return unicodeResults
|
||||
}
|
||||
}
|
||||
|
|
@ -259,6 +259,16 @@ class EmojiImpl {
|
|||
}
|
||||
}
|
||||
|
||||
fun shortcodeContains(query: String): List<Emoji> {
|
||||
return metadata.asSequence().map { group ->
|
||||
group.emoji.filter { emoji ->
|
||||
emoji.shortcodes.any { code ->
|
||||
code.contains(query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}.flatten().toList()
|
||||
}
|
||||
|
||||
init {
|
||||
metadata = initMetadata(RevoltApplication.instance.applicationContext)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue