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:
parent
dab8f0b51c
commit
5f787db4f1
|
@ -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 hide start call buttons from the room's toolbar
|
||||
- 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
|
||||
- Send freeform reactions
|
||||
- Render media captions ([MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530))
|
||||
|
|
|
@ -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 -->
|
||||
<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>
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.util
|
|||
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
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.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||
|
@ -114,7 +115,9 @@ sealed class MatrixItem(
|
|||
|
||||
data class EmoteItem(override val id: String,
|
||||
override val displayName: String? = null,
|
||||
override val avatarUrl: String? = null) :
|
||||
val emoteImage: EmoteImage,
|
||||
override val avatarUrl: String? = emoteImage.url,
|
||||
) :
|
||||
MatrixItem(id, displayName, avatarUrl) {
|
||||
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
package org.matrix.android.sdk.internal.session.room.send.pills
|
||||
|
||||
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.room.model.RoomEmoteContent.Companion.USAGE_STICKER
|
||||
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
||||
|
@ -130,3 +132,23 @@ fun CharSequence.requiresFormattedMessage(): Boolean {
|
|||
?: return false
|
||||
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
|
||||
}
|
||||
|
|
|
@ -253,7 +253,7 @@ class AutocompleteEmojiPresenter @AssistedInject constructor(
|
|||
}.filter {
|
||||
query == null || it.key.contains(query, true)
|
||||
}.map {
|
||||
EmojiItem(it.key, "", mxcUrl = it.value.url)
|
||||
EmojiItem(it.key, "", emoteImage = it.value)
|
||||
}.sortedBy { it.name }.distinctBy { it.mxcUrl }
|
||||
}
|
||||
|
||||
|
|
|
@ -67,11 +67,16 @@ class AutoCompleter @AssistedInject constructor(
|
|||
fun create(roomId: String, isInThreadTimeline: Boolean): AutoCompleter
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onAutoCompleteCustomEmote() {}
|
||||
}
|
||||
|
||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy {
|
||||
autocompleteCommandPresenterFactory.create(isInThreadTimeline)
|
||||
}
|
||||
|
||||
private var editText: EditText? = null
|
||||
private var callback: Callback? = null
|
||||
|
||||
fun enterSpecialMode() {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
|
@ -83,8 +88,9 @@ class AutoCompleter @AssistedInject constructor(
|
|||
|
||||
private lateinit var glideRequests: GlideRequests
|
||||
|
||||
fun setup(editText: EditText) {
|
||||
fun setup(editText: EditText, callback: Callback? = null) {
|
||||
this.editText = editText
|
||||
this.callback = callback
|
||||
glideRequests = GlideApp.with(editText)
|
||||
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, android.R.attr.colorBackground))
|
||||
setupCommands(backgroundDrawable, editText)
|
||||
|
@ -95,6 +101,7 @@ class AutoCompleter @AssistedInject constructor(
|
|||
|
||||
fun clear() {
|
||||
this.editText = null
|
||||
this.callback = null
|
||||
autocompleteEmojiPresenter.clear()
|
||||
autocompleteRoomPresenter.clear()
|
||||
autocompleteCommandPresenter.clear()
|
||||
|
@ -194,13 +201,13 @@ class AutoCompleter @AssistedInject constructor(
|
|||
|
||||
// Replace the word by its completion
|
||||
editable.delete(startIndex, endIndex)
|
||||
if (item.mxcUrl.isNotEmpty()) {
|
||||
if (item.emoteImage != null) {
|
||||
// Add emote html
|
||||
val emote = ":${item.name}:"
|
||||
editable.insert(startIndex, emote)
|
||||
|
||||
// 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(
|
||||
glideRequests,
|
||||
avatarRenderer,
|
||||
|
@ -210,6 +217,7 @@ class AutoCompleter @AssistedInject constructor(
|
|||
span.bind(editText)
|
||||
|
||||
editable.setSpan(span, startIndex, startIndex + emote.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
callback?.onAutoCompleteCustomEmote()
|
||||
} else {
|
||||
editable.insert(startIndex, item.emoji)
|
||||
}
|
||||
|
|
|
@ -676,13 +676,19 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
|
||||
if (room == null) return
|
||||
val content = initialState.rootThreadEventId?.let {
|
||||
action.stickerContent.copy(
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.THREAD,
|
||||
isFallingBack = true,
|
||||
eventId = it
|
||||
)
|
||||
)
|
||||
// Some sticker action might already have set this correctly, and maybe also done a real reply
|
||||
val actionRelatesTo = action.stickerContent.relatesTo
|
||||
if (actionRelatesTo?.type != RelationType.THREAD || actionRelatesTo.eventId != it) {
|
||||
action.stickerContent.copy(
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.THREAD,
|
||||
isFallingBack = true,
|
||||
eventId = it
|
||||
)
|
||||
)
|
||||
} else {
|
||||
action.stickerContent
|
||||
}
|
||||
} ?: action.stickerContent
|
||||
|
||||
room.sendService().sendEvent(EventType.STICKER, content.toContent())
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
|||
|
||||
sealed class MessageComposerAction : VectorViewModelAction {
|
||||
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 EnterQuoteMode(val eventId: String) : MessageComposerAction()
|
||||
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
|
||||
|
|
|
@ -103,6 +103,11 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
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.internal.session.room.send.pills.requiresFormattedMessage
|
||||
import reactivecircus.flowbinding.android.view.focusChanges
|
||||
|
@ -334,7 +339,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||
composerEditText.setHint(R.string.room_message_placeholder)
|
||||
|
||||
if (!isRichTextEditorEnabled) {
|
||||
autoCompleter.setup(composerEditText)
|
||||
autoCompleter.setup(composerEditText, composer)
|
||||
}
|
||||
|
||||
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() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EnterRegularMode(false))
|
||||
}
|
||||
|
|
|
@ -19,9 +19,11 @@ package im.vector.app.features.home.room.detail.composer
|
|||
import android.text.Editable
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import im.vector.app.features.home.room.detail.AutoCompleter
|
||||
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 {
|
||||
const val MAX_LINES_WHEN_COLLAPSED = 10
|
||||
|
@ -43,6 +45,7 @@ interface MessageComposerView {
|
|||
interface Callback : ComposerEditText.Callback {
|
||||
fun onCloseRelatedMessage()
|
||||
fun onSendMessage(text: CharSequence)
|
||||
fun onSendSticker(sticker: MatrixItem.EmoteItem)
|
||||
fun onAddAttachment()
|
||||
fun onExpandOrCompactChange()
|
||||
fun onFullScreenModeChanged()
|
||||
|
|
|
@ -144,6 +144,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
|
||||
// SC
|
||||
MessageComposerAction.ClearFocus -> _viewEvents.post(MessageComposerViewEvents.ClearFocus)
|
||||
MessageComposerAction.PopDraft -> popDraft(room)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.util.AttributeSet
|
|||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.toSpannable
|
||||
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.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.internal.session.room.send.pills.asSticker
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -76,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
private val views: ComposerLayoutScBinding
|
||||
|
||||
override var callback: Callback? = null
|
||||
private var modeSupportsSendAsSticker: Boolean = false
|
||||
|
||||
override val text: Editable?
|
||||
get() = views.composerEditText.text
|
||||
|
@ -110,6 +111,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
|
||||
override fun onTextChanged(text: CharSequence) {
|
||||
callback?.onTextChanged(text)
|
||||
updateSendStickerVisibility()
|
||||
}
|
||||
}
|
||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
|
@ -122,6 +124,11 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
callback?.onSendMessage(textMessage)
|
||||
}
|
||||
|
||||
views.sendStickerButton.setOnClickListener {
|
||||
val sticker = text?.asSticker() ?: return@setOnClickListener
|
||||
callback?.onSendSticker(sticker)
|
||||
}
|
||||
|
||||
views.attachmentButton.setOnClickListener {
|
||||
callback?.onAddAttachment()
|
||||
}
|
||||
|
@ -129,6 +136,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
|
||||
private fun collapse(transitionComplete: (() -> Unit)? = null) {
|
||||
views.relatedMessageGroup.isVisible = false
|
||||
updateSendStickerVisibility()
|
||||
transitionComplete?.invoke()
|
||||
callback?.onExpandOrCompactChange()
|
||||
|
||||
|
@ -137,21 +145,33 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
|
||||
private fun expand(transitionComplete: (() -> Unit)? = null) {
|
||||
views.relatedMessageGroup.isVisible = true
|
||||
updateSendStickerVisibility()
|
||||
transitionComplete?.invoke()
|
||||
callback?.onExpandOrCompactChange()
|
||||
|
||||
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 {
|
||||
return views.composerEditText.setTextIfDifferent(text)
|
||||
}
|
||||
|
||||
override fun onAutoCompleteCustomEmote() {
|
||||
updateSendStickerVisibility()
|
||||
}
|
||||
|
||||
override fun renderComposerMode(mode: MessageComposerMode, timelineViewModel: TimelineViewModel?) {
|
||||
val specialMode = mode as? MessageComposerMode.Special
|
||||
if (specialMode != null) {
|
||||
renderSpecialMode(specialMode, timelineViewModel)
|
||||
} else if (mode is MessageComposerMode.Normal) {
|
||||
modeSupportsSendAsSticker = true
|
||||
collapse()
|
||||
editText.setTextIfDifferent(mode.content)
|
||||
}
|
||||
|
@ -181,6 +201,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
)
|
||||
|
||||
private fun renderSpecialMode(specialMode: MessageComposerMode.Special, timelineViewModel: TimelineViewModel?) {
|
||||
modeSupportsSendAsSticker = specialMode is MessageComposerMode.Reply
|
||||
val event = specialMode.event
|
||||
val defaultContent = specialMode.defaultContent
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.reactions.data
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.room.model.EmoteImage
|
||||
|
||||
/**
|
||||
* Example:
|
||||
|
@ -42,11 +43,13 @@ data class EmojiItem(
|
|||
@Json(name = "a") val name: String,
|
||||
@Json(name = "b") val unicode: String,
|
||||
@Json(name = "j") val keywords: List<String> = emptyList(),
|
||||
val mxcUrl: String = ""
|
||||
val emoteImage: EmoteImage? = null,
|
||||
) {
|
||||
// Cannot be private...
|
||||
var cache: String? = null
|
||||
|
||||
val mxcUrl: String = emoteImage?.url ?: ""
|
||||
|
||||
val emoji: String
|
||||
get() {
|
||||
cache?.let { return it }
|
||||
|
|
|
@ -182,12 +182,39 @@
|
|||
android:src="@drawable/ic_attachment"
|
||||
app:tint="?android:textColorHint"
|
||||
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_constraintTop_toTopOf="@id/sendButton"
|
||||
app:layout_goneMarginBottom="57dp"
|
||||
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
|
||||
android:id="@+id/sendButton"
|
||||
android:layout_width="48dp"
|
||||
|
@ -199,7 +226,7 @@
|
|||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||
app:layout_constraintStart_toEndOf="@id/sendStickerButton"
|
||||
tools:ignore="MissingPrefix"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
|
Loading…
Reference in New Issue