Send-as-sticker button for sticker-enabled custom emotes

Add some primitive support for sending MSC2545 stickers, at least for
stickers that also support sending as custom emote.
Also, this introduces support to sending stickers as reply this way 🎉

Change-Id: I85b245c2c40b9662342459e50285c081d37f324b
This commit is contained in:
SpiritCroc 2023-04-01 12:35:15 +02:00
parent dab8f0b51c
commit 5f787db4f1
14 changed files with 155 additions and 19 deletions

View File

@ -30,7 +30,7 @@ Here you can find some extra features and changes compared to Element Android (w
- Setting to not alert for new messages if there's still an old notification for that room - Setting to not alert for new messages if there's still an old notification for that room
- Setting to hide start call buttons from the room's toolbar - Setting to hide start call buttons from the room's toolbar
- Render inline images / custom emojis in the timeline - Render inline images / custom emojis in the timeline
- Allow sending custom emotes, if they have been set up with another compatible client ([MSC2545](https://github.com/matrix-org/matrix-spec-proposals/pull/2545)) - Allow sending custom emotes (and partly stickers), if they have been set up with another compatible client ([MSC2545](https://github.com/matrix-org/matrix-spec-proposals/pull/2545))
- Render image reactions - Render image reactions
- Send freeform reactions - Send freeform reactions
- Render media captions ([MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530)) - Render media captions ([MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530))

View File

@ -227,4 +227,6 @@
<!-- Note to translators: the translation MUST contain the string "${app_name_sc_stable}", which will be replaced by the application name --> <!-- Note to translators: the translation MUST contain the string "${app_name_sc_stable}", which will be replaced by the application name -->
<string name="use_latest_app_sc">Use the latest ${app_name_sc_stable} on your other devices:</string> <string name="use_latest_app_sc">Use the latest ${app_name_sc_stable} on your other devices:</string>
<string name="action_send_as_sticker">Send as sticker</string>
</resources> </resources>

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.EmoteImage
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
@ -114,7 +115,9 @@ sealed class MatrixItem(
data class EmoteItem(override val id: String, data class EmoteItem(override val id: String,
override val displayName: String? = null, override val displayName: String? = null,
override val avatarUrl: String? = null) : val emoteImage: EmoteImage,
override val avatarUrl: String? = emoteImage.url,
) :
MatrixItem(id, displayName, avatarUrl) { MatrixItem(id, displayName, avatarUrl) {
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }

View File

@ -16,7 +16,9 @@
package org.matrix.android.sdk.internal.session.room.send.pills package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString import android.text.SpannableString
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.model.RoomEmoteContent.Companion.USAGE_STICKER
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
@ -130,3 +132,23 @@ fun CharSequence.requiresFormattedMessage(): Boolean {
?: return false ?: return false
return pills.isNotEmpty() return pills.isNotEmpty()
} }
fun CharSequence.asSticker(): MatrixItem.EmoteItem? {
val spannableString = SpannableString.valueOf(this)
val emotes = spannableString
?.getSpans(0, length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.filter { it.span.matrixItem is MatrixItem.EmoteItem }
if (emotes?.size == 1) {
val emote = emotes[0]
if (emote.start != 0 || emote.end != length) {
return null
}
val emoteItem = emote.span.matrixItem as MatrixItem.EmoteItem
val emoteImage = emoteItem.emoteImage
if (emoteImage.usage?.contains(USAGE_STICKER).orTrue()) {
return emoteItem
}
}
return null
}

View File

@ -253,7 +253,7 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
}.filter { }.filter {
query == null || it.key.contains(query, true) query == null || it.key.contains(query, true)
}.map { }.map {
EmojiItem(it.key, "", mxcUrl = it.value.url) EmojiItem(it.key, "", emoteImage = it.value)
}.sortedBy { it.name }.distinctBy { it.mxcUrl } }.sortedBy { it.name }.distinctBy { it.mxcUrl }
} }

View File

@ -67,11 +67,16 @@ class AutoCompleter @AssistedInject constructor(
fun create(roomId: String, isInThreadTimeline: Boolean): AutoCompleter fun create(roomId: String, isInThreadTimeline: Boolean): AutoCompleter
} }
interface Callback {
fun onAutoCompleteCustomEmote() {}
}
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy { private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy {
autocompleteCommandPresenterFactory.create(isInThreadTimeline) autocompleteCommandPresenterFactory.create(isInThreadTimeline)
} }
private var editText: EditText? = null private var editText: EditText? = null
private var callback: Callback? = null
fun enterSpecialMode() { fun enterSpecialMode() {
commandAutocompletePolicy.enabled = false commandAutocompletePolicy.enabled = false
@ -83,8 +88,9 @@ class AutoCompleter @AssistedInject constructor(
private lateinit var glideRequests: GlideRequests private lateinit var glideRequests: GlideRequests
fun setup(editText: EditText) { fun setup(editText: EditText, callback: Callback? = null) {
this.editText = editText this.editText = editText
this.callback = callback
glideRequests = GlideApp.with(editText) glideRequests = GlideApp.with(editText)
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, android.R.attr.colorBackground)) val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, android.R.attr.colorBackground))
setupCommands(backgroundDrawable, editText) setupCommands(backgroundDrawable, editText)
@ -95,6 +101,7 @@ class AutoCompleter @AssistedInject constructor(
fun clear() { fun clear() {
this.editText = null this.editText = null
this.callback = null
autocompleteEmojiPresenter.clear() autocompleteEmojiPresenter.clear()
autocompleteRoomPresenter.clear() autocompleteRoomPresenter.clear()
autocompleteCommandPresenter.clear() autocompleteCommandPresenter.clear()
@ -194,13 +201,13 @@ class AutoCompleter @AssistedInject constructor(
// Replace the word by its completion // Replace the word by its completion
editable.delete(startIndex, endIndex) editable.delete(startIndex, endIndex)
if (item.mxcUrl.isNotEmpty()) { if (item.emoteImage != null) {
// Add emote html // Add emote html
val emote = ":${item.name}:" val emote = ":${item.name}:"
editable.insert(startIndex, emote) editable.insert(startIndex, emote)
// Add span to make it look nice // Add span to make it look nice
val matrixItem = MatrixItem.EmoteItem(item.mxcUrl, item.name, item.mxcUrl) val matrixItem = MatrixItem.EmoteItem(item.emoteImage.url, item.name, item.emoteImage)
val span = PillImageSpan( val span = PillImageSpan(
glideRequests, glideRequests,
avatarRenderer, avatarRenderer,
@ -210,6 +217,7 @@ class AutoCompleter @AssistedInject constructor(
span.bind(editText) span.bind(editText)
editable.setSpan(span, startIndex, startIndex + emote.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) editable.setSpan(span, startIndex, startIndex + emote.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
callback?.onAutoCompleteCustomEmote()
} else { } else {
editable.insert(startIndex, item.emoji) editable.insert(startIndex, item.emoji)
} }

View File

@ -676,13 +676,19 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleSendSticker(action: RoomDetailAction.SendSticker) { private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
if (room == null) return if (room == null) return
val content = initialState.rootThreadEventId?.let { val content = initialState.rootThreadEventId?.let {
action.stickerContent.copy( // Some sticker action might already have set this correctly, and maybe also done a real reply
relatesTo = RelationDefaultContent( val actionRelatesTo = action.stickerContent.relatesTo
type = RelationType.THREAD, if (actionRelatesTo?.type != RelationType.THREAD || actionRelatesTo.eventId != it) {
isFallingBack = true, action.stickerContent.copy(
eventId = it relatesTo = RelationDefaultContent(
) type = RelationType.THREAD,
) isFallingBack = true,
eventId = it
)
)
} else {
action.stickerContent
}
} ?: action.stickerContent } ?: action.stickerContent
room.sendService().sendEvent(EventType.STICKER, content.toContent()) room.sendService().sendEvent(EventType.STICKER, content.toContent())

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
sealed class MessageComposerAction : VectorViewModelAction { sealed class MessageComposerAction : VectorViewModelAction {
data class SendMessage(val text: CharSequence, val formattedText: String?, val autoMarkdown: Boolean) : MessageComposerAction() data class SendMessage(val text: CharSequence, val formattedText: String?, val autoMarkdown: Boolean) : MessageComposerAction()
object PopDraft : MessageComposerAction() // SC
data class EnterEditMode(val eventId: String) : MessageComposerAction() data class EnterEditMode(val eventId: String) : MessageComposerAction()
data class EnterQuoteMode(val eventId: String) : MessageComposerAction() data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
data class EnterReplyMode(val eventId: String) : MessageComposerAction() data class EnterReplyMode(val eventId: String) : MessageComposerAction()

View File

@ -103,6 +103,11 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.room.send.pills.requiresFormattedMessage import org.matrix.android.sdk.internal.session.room.send.pills.requiresFormattedMessage
import reactivecircus.flowbinding.android.view.focusChanges import reactivecircus.flowbinding.android.view.focusChanges
@ -334,7 +339,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
composerEditText.setHint(R.string.room_message_placeholder) composerEditText.setHint(R.string.room_message_placeholder)
if (!isRichTextEditorEnabled) { if (!isRichTextEditorEnabled) {
autoCompleter.setup(composerEditText) autoCompleter.setup(composerEditText, composer)
} }
observerUserTyping() observerUserTyping()
@ -402,6 +407,40 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
} }
override fun onSendSticker(sticker: MatrixItem.EmoteItem) = withState(messageComposerViewModel) { state ->
val image = sticker.emoteImage
val sendMode = state.sendMode
val relatesTo = if (sendMode is SendMode.Reply) {
state.rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.THREAD,
eventId = it,
isFallingBack = false, // sendMode is reply, this reply is intentional and not a thread fallback
inReplyTo = ReplyToContent(eventId = sendMode.timelineEvent.eventId)
)
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = sendMode.timelineEvent.eventId))
} else {
null
}
val stickerContent = MessageStickerContent(
body = image.body ?: sticker.displayName ?: sticker.id,
info = image.info,
url = image.url,
relatesTo = relatesTo,
)
timelineViewModel.handle(RoomDetailAction.SendSticker(stickerContent))
if (state.isFullScreen) {
messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
}
messageComposerViewModel.handle(MessageComposerAction.PopDraft)
emojiPopup.dismiss()
if (vectorPreferences.jumpToBottomOnSend()) {
timelineViewModel.handle(RoomDetailAction.JumpToBottom)
}
}
override fun onCloseRelatedMessage() { override fun onCloseRelatedMessage() {
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(false)) messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(false))
} }

View File

@ -19,9 +19,11 @@ package im.vector.app.features.home.room.detail.composer
import android.text.Editable import android.text.Editable
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import im.vector.app.features.home.room.detail.AutoCompleter
import im.vector.app.features.home.room.detail.TimelineViewModel import im.vector.app.features.home.room.detail.TimelineViewModel
import org.matrix.android.sdk.api.util.MatrixItem
interface MessageComposerView { interface MessageComposerView : AutoCompleter.Callback {
companion object { companion object {
const val MAX_LINES_WHEN_COLLAPSED = 10 const val MAX_LINES_WHEN_COLLAPSED = 10
@ -43,6 +45,7 @@ interface MessageComposerView {
interface Callback : ComposerEditText.Callback { interface Callback : ComposerEditText.Callback {
fun onCloseRelatedMessage() fun onCloseRelatedMessage()
fun onSendMessage(text: CharSequence) fun onSendMessage(text: CharSequence)
fun onSendSticker(sticker: MatrixItem.EmoteItem)
fun onAddAttachment() fun onAddAttachment()
fun onExpandOrCompactChange() fun onExpandOrCompactChange()
fun onFullScreenModeChanged() fun onFullScreenModeChanged()

View File

@ -144,6 +144,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
// SC // SC
MessageComposerAction.ClearFocus -> _viewEvents.post(MessageComposerViewEvents.ClearFocus) MessageComposerAction.ClearFocus -> _viewEvents.post(MessageComposerViewEvents.ClearFocus)
MessageComposerAction.PopDraft -> popDraft(room)
} }
} }

View File

@ -24,7 +24,6 @@ import android.util.AttributeSet
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -54,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.session.room.send.pills.asSticker
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -76,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
private val views: ComposerLayoutScBinding private val views: ComposerLayoutScBinding
override var callback: Callback? = null override var callback: Callback? = null
private var modeSupportsSendAsSticker: Boolean = false
override val text: Editable? override val text: Editable?
get() = views.composerEditText.text get() = views.composerEditText.text
@ -110,6 +111,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
override fun onTextChanged(text: CharSequence) { override fun onTextChanged(text: CharSequence) {
callback?.onTextChanged(text) callback?.onTextChanged(text)
updateSendStickerVisibility()
} }
} }
views.composerRelatedMessageCloseButton.setOnClickListener { views.composerRelatedMessageCloseButton.setOnClickListener {
@ -122,6 +124,11 @@ class PlainTextComposerLayout @JvmOverloads constructor(
callback?.onSendMessage(textMessage) callback?.onSendMessage(textMessage)
} }
views.sendStickerButton.setOnClickListener {
val sticker = text?.asSticker() ?: return@setOnClickListener
callback?.onSendSticker(sticker)
}
views.attachmentButton.setOnClickListener { views.attachmentButton.setOnClickListener {
callback?.onAddAttachment() callback?.onAddAttachment()
} }
@ -129,6 +136,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
private fun collapse(transitionComplete: (() -> Unit)? = null) { private fun collapse(transitionComplete: (() -> Unit)? = null) {
views.relatedMessageGroup.isVisible = false views.relatedMessageGroup.isVisible = false
updateSendStickerVisibility()
transitionComplete?.invoke() transitionComplete?.invoke()
callback?.onExpandOrCompactChange() callback?.onExpandOrCompactChange()
@ -137,21 +145,33 @@ class PlainTextComposerLayout @JvmOverloads constructor(
private fun expand(transitionComplete: (() -> Unit)? = null) { private fun expand(transitionComplete: (() -> Unit)? = null) {
views.relatedMessageGroup.isVisible = true views.relatedMessageGroup.isVisible = true
updateSendStickerVisibility()
transitionComplete?.invoke() transitionComplete?.invoke()
callback?.onExpandOrCompactChange() callback?.onExpandOrCompactChange()
views.attachmentButton.isVisible = false views.attachmentButton.isVisible = false
} }
private fun updateSendStickerVisibility() {
val canSendAsSticker = modeSupportsSendAsSticker && views.composerEditText.text?.asSticker() != null
views.sendStickerButtonDecor.isVisible = canSendAsSticker
views.sendStickerButton.isVisible = canSendAsSticker
}
override fun setTextIfDifferent(text: CharSequence?): Boolean { override fun setTextIfDifferent(text: CharSequence?): Boolean {
return views.composerEditText.setTextIfDifferent(text) return views.composerEditText.setTextIfDifferent(text)
} }
override fun onAutoCompleteCustomEmote() {
updateSendStickerVisibility()
}
override fun renderComposerMode(mode: MessageComposerMode, timelineViewModel: TimelineViewModel?) { override fun renderComposerMode(mode: MessageComposerMode, timelineViewModel: TimelineViewModel?) {
val specialMode = mode as? MessageComposerMode.Special val specialMode = mode as? MessageComposerMode.Special
if (specialMode != null) { if (specialMode != null) {
renderSpecialMode(specialMode, timelineViewModel) renderSpecialMode(specialMode, timelineViewModel)
} else if (mode is MessageComposerMode.Normal) { } else if (mode is MessageComposerMode.Normal) {
modeSupportsSendAsSticker = true
collapse() collapse()
editText.setTextIfDifferent(mode.content) editText.setTextIfDifferent(mode.content)
} }
@ -181,6 +201,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
) )
private fun renderSpecialMode(specialMode: MessageComposerMode.Special, timelineViewModel: TimelineViewModel?) { private fun renderSpecialMode(specialMode: MessageComposerMode.Special, timelineViewModel: TimelineViewModel?) {
modeSupportsSendAsSticker = specialMode is MessageComposerMode.Reply
val event = specialMode.event val event = specialMode.event
val defaultContent = specialMode.defaultContent val defaultContent = specialMode.defaultContent

View File

@ -18,6 +18,7 @@ package im.vector.app.features.reactions.data
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.room.model.EmoteImage
/** /**
* Example: * Example:
@ -42,11 +43,13 @@ data class EmojiItem(
@Json(name = "a") val name: String, @Json(name = "a") val name: String,
@Json(name = "b") val unicode: String, @Json(name = "b") val unicode: String,
@Json(name = "j") val keywords: List<String> = emptyList(), @Json(name = "j") val keywords: List<String> = emptyList(),
val mxcUrl: String = "" val emoteImage: EmoteImage? = null,
) { ) {
// Cannot be private... // Cannot be private...
var cache: String? = null var cache: String? = null
val mxcUrl: String = emoteImage?.url ?: ""
val emoji: String val emoji: String
get() { get() {
cache?.let { return it } cache?.let { return it }

View File

@ -182,12 +182,39 @@
android:src="@drawable/ic_attachment" android:src="@drawable/ic_attachment"
app:tint="?android:textColorHint" app:tint="?android:textColorHint"
app:layout_constraintBottom_toBottomOf="@id/sendButton" app:layout_constraintBottom_toBottomOf="@id/sendButton"
app:layout_constraintEnd_toStartOf="@id/sendButton" app:layout_constraintEnd_toStartOf="@id/sendStickerButton"
app:layout_constraintStart_toEndOf="@id/composerEditText" app:layout_constraintStart_toEndOf="@id/composerEditText"
app:layout_constraintTop_toTopOf="@id/sendButton" app:layout_constraintTop_toTopOf="@id/sendButton"
app:layout_goneMarginBottom="57dp" app:layout_goneMarginBottom="57dp"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix" />
<ImageButton
android:id="@+id/sendStickerButton"
android:layout_width="48dp"
android:layout_height="@dimen/composer_min_height"
android:contentDescription="@string/action_send_as_sticker"
android:scaleType="center"
android:src="@drawable/ic_send"
android:background="?android:attr/selectableItemBackground"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/sendButton"
app:layout_constraintStart_toEndOf="@id/attachmentButton"
app:layout_constraintTop_toTopOf="@id/sendButton"
app:layout_constraintBottom_toBottomOf="@id/sendButton"
tools:ignore="MissingPrefix"
tools:visibility="visible" />
<ImageView
android:id="@+id/sendStickerButtonDecor"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_attachment_sticker"
android:visibility="gone"
app:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="@id/sendStickerButton"
app:layout_constraintEnd_toEndOf="@id/sendStickerButton"
tools:visibility="visible" />
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="48dp" android:layout_width="48dp"
@ -199,7 +226,7 @@
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attachmentButton" app:layout_constraintStart_toEndOf="@id/sendStickerButton"
tools:ignore="MissingPrefix" tools:ignore="MissingPrefix"
tools:visibility="visible" /> tools:visibility="visible" />