diff --git a/FEATURES.md b/FEATURES.md
index aa6c117c58..61168608f6 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -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))
diff --git a/library/ui-strings/src/main/res/values/strings_sc.xml b/library/ui-strings/src/main/res/values/strings_sc.xml
index a5da1759a0..cfc39ee8b6 100644
--- a/library/ui-strings/src/main/res/values/strings_sc.xml
+++ b/library/ui-strings/src/main/res/values/strings_sc.xml
@@ -227,4 +227,6 @@
Use the latest ${app_name_sc_stable} on your other devices:
+ Send as sticker
+
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 41f7cde3a3..6746dba8dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -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)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index fa1840f520..a74ad62007 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -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
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
index ac094a9c2c..95d61a08cd 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt
@@ -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 }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
index 17eeb1d48d..6c858cfcc5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
@@ -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)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 25a62cc6d8..4751a2de85 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -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())
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
index 61bd7953f4..ba307b631a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
@@ -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()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index 14970db365..89b402597d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -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(), 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(), 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))
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
index 9174dc383c..4202756670 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
@@ -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()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 7e30e68631..dcdcbed201 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -144,6 +144,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
// SC
MessageComposerAction.ClearFocus -> _viewEvents.post(MessageComposerViewEvents.ClearFocus)
+ MessageComposerAction.PopDraft -> popDraft(room)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
index e13b26c172..6ec504751f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiItem.kt b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiItem.kt
index 905c9cb73c..583b24c347 100644
--- a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiItem.kt
+++ b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiItem.kt
@@ -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 = 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 }
diff --git a/vector/src/main/res/layout/composer_layout_sc.xml b/vector/src/main/res/layout/composer_layout_sc.xml
index 6a12229155..6fed340a90 100644
--- a/vector/src/main/res/layout/composer_layout_sc.xml
+++ b/vector/src/main/res/layout/composer_layout_sc.xml
@@ -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" />
+
+
+
+