mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Send mention pills from composer
This commit is contained in:
parent
bf9ce4f690
commit
6bd7257cf2
13 changed files with 141 additions and 42 deletions
|
@ -72,7 +72,7 @@ interface RelationService {
|
||||||
*/
|
*/
|
||||||
fun editTextMessage(targetEventId: String,
|
fun editTextMessage(targetEventId: String,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||||
|
|
||||||
|
@ -97,12 +97,14 @@ interface RelationService {
|
||||||
/**
|
/**
|
||||||
* Reply to an event in the timeline (must be in same room)
|
* Reply to an event in the timeline (must be in same room)
|
||||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
||||||
|
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
|
||||||
|
* by the sdk into pills.
|
||||||
* @param eventReplied the event referenced by the reply
|
* @param eventReplied the event referenced by the reply
|
||||||
* @param replyText the reply text
|
* @param replyText the reply text
|
||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
*/
|
*/
|
||||||
fun replyToMessage(eventReplied: TimelineEvent,
|
fun replyToMessage(eventReplied: TimelineEvent,
|
||||||
replyText: String,
|
replyText: CharSequence,
|
||||||
autoMarkdown: Boolean = false): Cancelable?
|
autoMarkdown: Boolean = false): Cancelable?
|
||||||
|
|
||||||
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
||||||
|
|
|
@ -29,12 +29,14 @@ interface SendService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a text message asynchronously.
|
* Method to send a text message asynchronously.
|
||||||
|
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
|
||||||
|
* by the sdk into pills.
|
||||||
* @param text the text message to send
|
* @param text the text message to send
|
||||||
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
|
fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a text message with a formatted body.
|
* Method to send a text message with a formatted body.
|
||||||
|
@ -42,7 +44,7 @@ interface SendService {
|
||||||
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable
|
fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a media asynchronously.
|
* Method to send a media asynchronously.
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.api.session.room.send
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class for spans that should mention a user.
|
||||||
|
* These Spans will be transformed into pills when detected in message to send
|
||||||
|
*/
|
||||||
|
interface UserMentionSpan {
|
||||||
|
abstract val displayName: String
|
||||||
|
abstract val userId: String
|
||||||
|
}
|
|
@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
|
|
||||||
override fun editTextMessage(targetEventId: String,
|
override fun editTextMessage(targetEventId: String,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String): Cancelable {
|
compatibilityBodyText: String): Cancelable {
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
|
@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
|
override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
|
||||||
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
|
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
|
||||||
?.also { saveLocalEcho(it) }
|
?.also { saveLocalEcho(it) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
|
@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
|
||||||
|
|
||||||
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
|
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
|
||||||
return sendEvent(event)
|
return sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
|
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also {
|
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.text.SpannableString
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
|
@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
||||||
|
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
||||||
|
@ -58,37 +60,67 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
|
||||||
// TODO Inject
|
// TODO Inject
|
||||||
private val renderer = HtmlRenderer.builder().build()
|
private val renderer = HtmlRenderer.builder().build()
|
||||||
|
|
||||||
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
|
fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
|
||||||
if (msgType == MessageType.MSGTYPE_TEXT) {
|
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
|
||||||
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
|
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
|
||||||
}
|
}
|
||||||
val content = MessageTextContent(type = msgType, body = text)
|
val content = MessageTextContent(type = msgType, body = text.toString())
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent {
|
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
|
||||||
if (autoMarkdown) {
|
if (autoMarkdown) {
|
||||||
val document = parser.parse(text)
|
val source = transformPills(text,"[%2\$s](https://matrix.to/#/%1\$s)") ?: text.toString()
|
||||||
|
val document = parser.parse(source)
|
||||||
val htmlText = renderer.render(document)
|
val htmlText = renderer.render(document)
|
||||||
|
|
||||||
if (isFormattedTextPertinent(text, htmlText)) {
|
if (isFormattedTextPertinent(source, htmlText)) {
|
||||||
return TextContent(text, htmlText)
|
return TextContent(source, htmlText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Try to detect pills
|
||||||
|
transformPills(text, "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>")?.let {
|
||||||
|
return TextContent(text.toString(),it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextContent(text)
|
return TextContent(text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformPills(text: CharSequence,
|
||||||
|
template : String)
|
||||||
|
: String? {
|
||||||
|
val bufSB = StringBuffer()
|
||||||
|
var currIndex = 0
|
||||||
|
SpannableString.valueOf(text).let {
|
||||||
|
val pills = it.getSpans(0, text.length, UserMentionSpan::class.java)
|
||||||
|
if (pills.isNotEmpty()) {
|
||||||
|
pills.forEachIndexed { _, urlSpan ->
|
||||||
|
val start = it.getSpanStart(urlSpan)
|
||||||
|
val end = it.getSpanEnd(urlSpan)
|
||||||
|
//We want to replace with the pill with a html link
|
||||||
|
bufSB.append(text, currIndex, start)
|
||||||
|
bufSB.append(String.format(template,urlSpan.userId,urlSpan.displayName))
|
||||||
|
currIndex = end
|
||||||
|
}
|
||||||
|
bufSB.append(text, currIndex, text.length)
|
||||||
|
return bufSB.toString()
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
||||||
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
||||||
|
|
||||||
fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
|
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
|
||||||
return createEvent(roomId, textContent.toMessageTextContent())
|
return createEvent(roomId, textContent.toMessageTextContent(msgType))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReplaceTextEvent(roomId: String,
|
fun createReplaceTextEvent(roomId: String,
|
||||||
targetEventId: String,
|
targetEventId: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
compatibilityText: String): Event {
|
compatibilityText: String): Event {
|
||||||
|
@ -279,7 +311,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
|
||||||
return System.currentTimeMillis()
|
return System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? {
|
fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? {
|
||||||
// Fallbacks and event representation
|
// Fallbacks and event representation
|
||||||
// TODO Add error/warning logs when any of this is null
|
// TODO Add error/warning logs when any of this is null
|
||||||
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
||||||
|
@ -298,7 +330,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
|
||||||
//
|
//
|
||||||
// > <@alice:example.org> This is the original body
|
// > <@alice:example.org> This is the original body
|
||||||
//
|
//
|
||||||
val replyFallback = buildReplyFallback(body, userId, replyText)
|
val replyFallback = buildReplyFallback(body, userId, replyText.toString())
|
||||||
|
|
||||||
val eventId = eventReplied.root.eventId ?: return null
|
val eventId = eventReplied.root.eventId ?: return null
|
||||||
val content = MessageTextContent(
|
val content = MessageTextContent(
|
||||||
|
|
|
@ -27,7 +27,7 @@ object CommandParser {
|
||||||
* @param textMessage the text message
|
* @param textMessage the text message
|
||||||
* @return a parsed slash command (ok or error)
|
* @return a parsed slash command (ok or error)
|
||||||
*/
|
*/
|
||||||
fun parseSplashCommand(textMessage: String): ParsedCommand {
|
fun parseSplashCommand(textMessage: CharSequence): ParsedCommand {
|
||||||
// check if it has the Slash marker
|
// check if it has the Slash marker
|
||||||
if (!textMessage.startsWith("/")) {
|
if (!textMessage.startsWith("/")) {
|
||||||
return ParsedCommand.ErrorNotACommand
|
return ParsedCommand.ErrorNotACommand
|
||||||
|
@ -76,7 +76,7 @@ object CommandParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command.EMOTE.command -> {
|
Command.EMOTE.command -> {
|
||||||
val message = textMessage.substring(Command.EMOTE.command.length).trim()
|
val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim()
|
||||||
|
|
||||||
ParsedCommand.SendEmote(message)
|
ParsedCommand.SendEmote(message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ sealed class ParsedCommand {
|
||||||
|
|
||||||
// Valid commands:
|
// Valid commands:
|
||||||
|
|
||||||
class SendEmote(val message: String) : ParsedCommand()
|
class SendEmote(val message: CharSequence) : ParsedCommand()
|
||||||
class BanUser(val userId: String, val reason: String) : ParsedCommand()
|
class BanUser(val userId: String, val reason: String) : ParsedCommand()
|
||||||
class UnbanUser(val userId: String) : ParsedCommand()
|
class UnbanUser(val userId: String) : ParsedCommand()
|
||||||
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
|
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
|
||||||
|
|
|
@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class RoomDetailAction : VectorViewModelAction {
|
sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
data class SaveDraft(val draft: String) : RoomDetailAction()
|
data class SaveDraft(val draft: String) : RoomDetailAction()
|
||||||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailAction()
|
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
|
||||||
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction()
|
||||||
|
|
|
@ -28,6 +28,7 @@ import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -609,7 +610,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendMessage(text: String) {
|
override fun onSendMessage(text: CharSequence) {
|
||||||
if (lockSendButton) {
|
if (lockSendButton) {
|
||||||
Timber.w("Send button is locked")
|
Timber.w("Send button is locked")
|
||||||
return
|
return
|
||||||
|
@ -977,7 +978,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||||
insertUserDisplayNameInTextEditor(informationData.memberName?.toString())
|
session.getUser(informationData.senderId)?.let {
|
||||||
|
insertUserDisplayNameInTextEditor(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
||||||
|
@ -1166,8 +1169,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
* @param text the text to insert.
|
* @param text the text to insert.
|
||||||
*/
|
*/
|
||||||
// TODO legacy, refactor
|
// TODO legacy, refactor
|
||||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
private fun insertUserDisplayNameInTextEditor(member: User) {
|
||||||
// TODO move logic outside of fragment
|
// TODO move logic outside of fragment
|
||||||
|
val text = member.displayName
|
||||||
if (null != text) {
|
if (null != text) {
|
||||||
// var vibrate = false
|
// var vibrate = false
|
||||||
|
|
||||||
|
@ -1176,19 +1180,44 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// current user
|
// current user
|
||||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
|
||||||
|
?: 0)
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// another user
|
// another user
|
||||||
|
val sanitizeDisplayName = sanitizeDisplayName(text)
|
||||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||||
// Ensure displayName will not be interpreted as a Slash command
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
if (text.startsWith("/")) {
|
if (text.startsWith("/")) {
|
||||||
composerLayout.composerEditText.append("\\")
|
composerLayout.composerEditText.append("\\")
|
||||||
}
|
}
|
||||||
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
SpannableStringBuilder().apply {
|
||||||
|
append(sanitizeDisplayName)
|
||||||
|
setSpan(
|
||||||
|
PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member),
|
||||||
|
0,
|
||||||
|
sanitizeDisplayName.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
append(": ")
|
||||||
|
}.let {
|
||||||
|
composerLayout.composerEditText.append(it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
SpannableStringBuilder().apply {
|
||||||
|
append(sanitizeDisplayName)
|
||||||
|
setSpan(
|
||||||
|
PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member),
|
||||||
|
0,
|
||||||
|
sanitizeDisplayName.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
append(" ")
|
||||||
|
}.let {
|
||||||
|
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, it)
|
||||||
|
}
|
||||||
|
// composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName + " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
|
@ -165,6 +166,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
invisibleEventsObservable.accept(action)
|
invisibleEventsObservable.accept(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMember(userId: String) : RoomMember? {
|
||||||
|
return room.getRoomMember(userId)
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Convert a send mode to a draft and save the draft
|
* Convert a send mode to a draft and save the draft
|
||||||
*/
|
*/
|
||||||
|
@ -355,7 +359,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
// TODO check if same content?
|
// TODO check if same content?
|
||||||
room.getTimeLineEvent(inReplyTo)?.let {
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
room.editReply(state.sendMode.timelineEvent, it, action.text)
|
room.editReply(state.sendMode.timelineEvent, it, action.text.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
|
@ -380,7 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
|
|
||||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
||||||
|
|
||||||
|
//TODO check for pills?
|
||||||
|
|
||||||
// TODO Refactor this, just temporary for quotes
|
// TODO Refactor this, just temporary for quotes
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
|
@ -397,7 +403,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
is SendMode.REPLY -> {
|
is SendMode.REPLY -> {
|
||||||
state.sendMode.timelineEvent.let {
|
state.sendMode.timelineEvent.let {
|
||||||
room.replyToMessage(it, action.text, action.autoMarkdown)
|
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
|
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
import androidx.transition.AutoTransition
|
import androidx.transition.AutoTransition
|
||||||
import androidx.transition.Transition
|
import androidx.transition.Transition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
|
|
||||||
interface Callback : ComposerEditText.Callback {
|
interface Callback : ComposerEditText.Callback {
|
||||||
fun onCloseRelatedMessage()
|
fun onCloseRelatedMessage()
|
||||||
fun onSendMessage(text: String)
|
fun onSendMessage(text: CharSequence)
|
||||||
fun onAddAttachment()
|
fun onAddAttachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
}
|
}
|
||||||
|
|
||||||
sendButton.setOnClickListener {
|
sendButton.setOnClickListener {
|
||||||
val textMessage = text?.toString() ?: ""
|
val textMessage = text?.toSpannable()
|
||||||
callback?.onSendMessage(textMessage)
|
callback?.onSendMessage(textMessage ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentButton.setOnClickListener {
|
attachmentButton.setOnClickListener {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import androidx.annotation.UiThread
|
||||||
import com.bumptech.glide.request.target.SimpleTarget
|
import com.bumptech.glide.request.target.SimpleTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.google.android.material.chip.ChipDrawable
|
import com.google.android.material.chip.ChipDrawable
|
||||||
|
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.glide.GlideRequests
|
import im.vector.riotx.core.glide.GlideRequests
|
||||||
|
@ -37,14 +38,15 @@ import java.lang.ref.WeakReference
|
||||||
/**
|
/**
|
||||||
* This span is able to replace a text by a [ChipDrawable]
|
* This span is able to replace a text by a [ChipDrawable]
|
||||||
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
||||||
|
* Implements UserMentionSpan so that it could be automatically transformed in matrix links and displayed as pills.
|
||||||
*/
|
*/
|
||||||
class PillImageSpan(private val glideRequests: GlideRequests,
|
class PillImageSpan(private val glideRequests: GlideRequests,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val userId: String,
|
override val userId: String,
|
||||||
private val user: User?) : ReplacementSpan() {
|
private val user: User?) : ReplacementSpan(), UserMentionSpan {
|
||||||
|
|
||||||
private val displayName by lazy {
|
override val displayName by lazy {
|
||||||
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
|
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue