Merge pull request #687 from vector-im/feature/dat_pill
Send mention pills from composer
This commit is contained in:
commit
ebf21fe9d8
|
@ -5,7 +5,7 @@ Features ✨:
|
||||||
-
|
-
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- Send mention Pills from composer
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
- Fix a small grammatical error when an empty room list is shown.
|
- Fix a small grammatical error when an empty room list is shown.
|
||||||
|
|
|
@ -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,20 +29,23 @@ 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.
|
||||||
* @param text the text message to send
|
* @param text the text message to send
|
||||||
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
||||||
|
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
||||||
* @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,26 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
val displayName: String
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
|
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
|
||||||
import im.vector.matrix.android.internal.util.StringProvider
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
|
@ -50,45 +51,55 @@ import javax.inject.Inject
|
||||||
*
|
*
|
||||||
* The transactionID is used as loc
|
* The transactionID is used as loc
|
||||||
*/
|
*/
|
||||||
internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String,
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
@UserId private val userId: String,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater) {
|
private val stringProvider: StringProvider,
|
||||||
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
|
private val textPillsUtils: TextPillsUtils
|
||||||
|
) {
|
||||||
// TODO Inject
|
// TODO Inject
|
||||||
private val parser = Parser.builder().build()
|
private val parser = Parser.builder().build()
|
||||||
// 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 = textPillsUtils.processSpecialSpansToMarkdown(text)
|
||||||
|
?: 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
|
||||||
|
textPillsUtils.processSpecialSpansToHtml(text)?.let {
|
||||||
|
return TextContent(text.toString(), it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextContent(text)
|
return TextContent(text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +290,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 +309,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(
|
||||||
|
|
|
@ -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.internal.session.room.send.pills
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
|
||||||
|
|
||||||
|
internal data class MentionLinkSpec(
|
||||||
|
val span: UserMentionSpan,
|
||||||
|
val start: Int,
|
||||||
|
val end: Int
|
||||||
|
)
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.internal.session.room.send.pills
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class MentionLinkSpecComparator @Inject constructor() : Comparator<MentionLinkSpec> {
|
||||||
|
|
||||||
|
override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int {
|
||||||
|
return when {
|
||||||
|
o1.start < o2.start -> -1
|
||||||
|
o1.start > o2.start -> 1
|
||||||
|
o1.end < o2.end -> 1
|
||||||
|
o1.end > o2.end -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* 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.internal.session.room.send.pills
|
||||||
|
|
||||||
|
import android.text.SpannableString
|
||||||
|
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to detect special span in CharSequence and turn them into
|
||||||
|
* formatted text to send them as a Matrix messages.
|
||||||
|
*
|
||||||
|
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
|
||||||
|
*/
|
||||||
|
internal class TextPillsUtils @Inject constructor(
|
||||||
|
private val mentionLinkSpecComparator: MentionLinkSpecComparator
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if transformable spans are present in the text.
|
||||||
|
* @return the transformed String or null if no Span found
|
||||||
|
*/
|
||||||
|
fun processSpecialSpansToHtml(text: CharSequence): String? {
|
||||||
|
return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if transformable spans are present in the text.
|
||||||
|
* @return the transformed String or null if no Span found
|
||||||
|
*/
|
||||||
|
fun processSpecialSpansToMarkdown(text: CharSequence): String? {
|
||||||
|
return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformPills(text: CharSequence, template: String): String? {
|
||||||
|
val spannableString = SpannableString.valueOf(text)
|
||||||
|
val pills = spannableString
|
||||||
|
?.getSpans(0, text.length, UserMentionSpan::class.java)
|
||||||
|
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
|
||||||
|
?.toMutableList()
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
// we need to prune overlaps!
|
||||||
|
pruneOverlaps(pills)
|
||||||
|
|
||||||
|
return buildString {
|
||||||
|
var currIndex = 0
|
||||||
|
pills.forEachIndexed { _, (urlSpan, start, end) ->
|
||||||
|
// We want to replace with the pill with a html link
|
||||||
|
append(text, currIndex, start)
|
||||||
|
append(String.format(template, urlSpan.userId, urlSpan.displayName))
|
||||||
|
currIndex = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pruneOverlaps(links: MutableList<MentionLinkSpec>) {
|
||||||
|
Collections.sort(links, mentionLinkSpecComparator)
|
||||||
|
var len = links.size
|
||||||
|
var i = 0
|
||||||
|
while (i < len - 1) {
|
||||||
|
val a = links[i]
|
||||||
|
val b = links[i + 1]
|
||||||
|
var remove = -1
|
||||||
|
|
||||||
|
// test if there is an overlap
|
||||||
|
if (b.start in a.start until a.end) {
|
||||||
|
when {
|
||||||
|
b.end <= a.end ->
|
||||||
|
// b is inside a -> b should be removed
|
||||||
|
remove = i + 1
|
||||||
|
a.end - a.start > b.end - b.start ->
|
||||||
|
// overlap and a is bigger -> b should be removed
|
||||||
|
remove = i + 1
|
||||||
|
a.end - a.start < b.end - b.start ->
|
||||||
|
// overlap and a is smaller -> a should be removed
|
||||||
|
remove = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove != -1) {
|
||||||
|
links.removeAt(remove)
|
||||||
|
len--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
|
||||||
|
|
||||||
|
private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||||
|
|
||||||
|
inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply argument to a Fragment
|
* Apply argument to a Fragment
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,6 +21,14 @@ import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
|
||||||
fun View.hideKeyboard() {
|
fun View.hideKeyboard() {
|
||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.showKeyboard(andRequestFocus: Boolean = false) {
|
||||||
|
if (andRequestFocus) {
|
||||||
|
requestFocus()
|
||||||
|
}
|
||||||
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -16,10 +16,8 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.createdirect
|
package im.vector.riotx.features.home.createdirect
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import com.airbnb.mvrx.activityViewModel
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
|
@ -27,6 +25,7 @@ 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.extensions.hideKeyboard
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.setupAsSearch
|
import im.vector.riotx.core.extensions.setupAsSearch
|
||||||
|
import im.vector.riotx.core.extensions.showKeyboard
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
|
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -63,9 +62,7 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
|
||||||
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString()))
|
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString()))
|
||||||
}
|
}
|
||||||
.disposeOnDestroyView()
|
.disposeOnDestroyView()
|
||||||
createDirectRoomSearchById.requestFocus()
|
createDirectRoomSearchById.showKeyboard(andRequestFocus = true)
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
||||||
imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupCloseView() {
|
private fun setupCloseView() {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
@ -29,7 +28,6 @@ import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
@ -37,6 +35,7 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
|
@ -73,6 +72,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.hideKeyboard
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.setTextOrHide
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
|
import im.vector.riotx.core.extensions.showKeyboard
|
||||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
@ -158,7 +158,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**x
|
/**
|
||||||
* Sanitize the display name.
|
* Sanitize the display name.
|
||||||
*
|
*
|
||||||
* @param displayName the display name to sanitize
|
* @param displayName the display name to sanitize
|
||||||
|
@ -405,7 +405,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar)
|
avatarRenderer.render(
|
||||||
|
event.senderAvatar,
|
||||||
|
event.root.senderId ?: "",
|
||||||
|
event.getDisambiguatedDisplayName(),
|
||||||
|
composerLayout.composerRelatedMessageAvatar
|
||||||
|
)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
// need to do it here also when not using quick reply
|
// need to do it here also when not using quick reply
|
||||||
focusComposerAndShowKeyboard()
|
focusComposerAndShowKeyboard()
|
||||||
|
@ -588,7 +593,13 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
// Add the span
|
// Add the span
|
||||||
val user = session.getUser(item.userId)
|
val user = session.getUser(item.userId)
|
||||||
val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user)
|
val span = PillImageSpan(
|
||||||
|
glideRequests,
|
||||||
|
avatarRenderer,
|
||||||
|
requireContext(),
|
||||||
|
item.userId,
|
||||||
|
user?.displayName ?: item.userId,
|
||||||
|
user?.avatarUrl)
|
||||||
span.bind(composerLayout.composerEditText)
|
span.bind(composerLayout.composerEditText)
|
||||||
|
|
||||||
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
@ -609,7 +620,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
|
||||||
|
@ -975,9 +986,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
vectorBaseActivity.notImplemented("Click on user avatar")
|
vectorBaseActivity.notImplemented("Click on user avatar")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||||
insertUserDisplayNameInTextEditor(informationData.memberName?.toString())
|
insertUserDisplayNameInTextEditor(informationData.senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
||||||
|
@ -1159,55 +1169,61 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils
|
|
||||||
/**
|
/**
|
||||||
* Insert an user displayname in the message editor.
|
* Insert a user displayName in the message editor.
|
||||||
*
|
*
|
||||||
* @param text the text to insert.
|
* @param userId the userId.
|
||||||
*/
|
*/
|
||||||
// TODO legacy, refactor
|
@SuppressLint("SetTextI18n")
|
||||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
private fun insertUserDisplayNameInTextEditor(userId: String) {
|
||||||
// TODO move logic outside of fragment
|
val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
|
||||||
if (null != text) {
|
|
||||||
// var vibrate = false
|
|
||||||
|
|
||||||
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
if (startToCompose
|
||||||
if (myDisplayName == text) {
|
&& userId == session.myUserId) {
|
||||||
// current user
|
// Empty composer, current user: start an emote
|
||||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
|
||||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1)
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
} else {
|
||||||
// vibrate = true
|
val roomMember = roomDetailViewModel.getMember(userId)
|
||||||
}
|
// TODO move logic outside of fragment
|
||||||
} else {
|
(roomMember?.displayName ?: userId)
|
||||||
// another user
|
.let { sanitizeDisplayName(it) }
|
||||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
.let { displayName ->
|
||||||
// Ensure displayName will not be interpreted as a Slash command
|
buildSpannedString {
|
||||||
if (text.startsWith("/")) {
|
append(displayName)
|
||||||
composerLayout.composerEditText.append("\\")
|
setSpan(
|
||||||
|
PillImageSpan(
|
||||||
|
glideRequests,
|
||||||
|
avatarRenderer,
|
||||||
|
requireContext(),
|
||||||
|
userId,
|
||||||
|
displayName,
|
||||||
|
roomMember?.avatarUrl)
|
||||||
|
.also { it.bind(composerLayout.composerEditText) },
|
||||||
|
0,
|
||||||
|
displayName.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
append(if (startToCompose) ": " else " ")
|
||||||
|
}.let { pill ->
|
||||||
|
if (startToCompose) {
|
||||||
|
if (displayName.startsWith("/")) {
|
||||||
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
|
composerLayout.composerEditText.append("\\")
|
||||||
|
}
|
||||||
|
composerLayout.composerEditText.append(pill)
|
||||||
|
} else {
|
||||||
|
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, pill)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
|
||||||
} else {
|
|
||||||
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// vibrate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (vibrate && vectorPreferences.vibrateWhenMentioning()) {
|
|
||||||
// val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
||||||
// if (v?.hasVibrator() == true) {
|
|
||||||
// v.vibrate(100)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
focusComposerAndShowKeyboard()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusComposerAndShowKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun focusComposerAndShowKeyboard() {
|
private fun focusComposerAndShowKeyboard() {
|
||||||
composerLayout.composerEditText.requestFocus()
|
composerLayout.composerEditText.showKeyboard(andRequestFocus = true)
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
||||||
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,17 @@ package im.vector.riotx.features.home.room.detail.composer
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.text.Editable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
|
import im.vector.riotx.core.extensions.ooi
|
||||||
|
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||||
|
import im.vector.riotx.features.html.PillImageSpan
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
||||||
: AppCompatEditText(context, attrs, defStyleAttr) {
|
: AppCompatEditText(context, attrs, defStyleAttr) {
|
||||||
|
@ -55,4 +60,41 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
}
|
}
|
||||||
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
|
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
addTextChangedListener(
|
||||||
|
object : SimpleTextWatcher() {
|
||||||
|
var spanToRemove: PillImageSpan? = null
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||||
|
Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after")
|
||||||
|
|
||||||
|
if (count > after) {
|
||||||
|
// A char has been deleted
|
||||||
|
val deleteCharPosition = start + count
|
||||||
|
Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition")
|
||||||
|
|
||||||
|
// Get the first span at this position
|
||||||
|
spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java)
|
||||||
|
.ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
if (spanToRemove != null) {
|
||||||
|
val start = editableText.getSpanStart(spanToRemove)
|
||||||
|
val end = editableText.getSpanEnd(spanToRemove)
|
||||||
|
Timber.v("Pills: afterTextChanged Removing the span start:$start end:$end")
|
||||||
|
// Must be done before text replacement
|
||||||
|
editableText.removeSpan(spanToRemove)
|
||||||
|
if (start != -1 && end != -1) {
|
||||||
|
editableText.replace(start, end, "")
|
||||||
|
}
|
||||||
|
spanToRemove = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +87,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests,
|
||||||
when (permalinkData) {
|
when (permalinkData) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)
|
||||||
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user)
|
val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user?.displayName
|
||||||
|
?: permalinkData.userId, user?.avatarUrl)
|
||||||
SpannableBuilder.setSpans(
|
SpannableBuilder.setSpans(
|
||||||
visitor.builder(),
|
visitor.builder(),
|
||||||
span,
|
span,
|
||||||
|
|
|
@ -28,7 +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.user.model.User
|
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.glide.GlideRequests
|
import im.vector.riotx.core.glide.GlideRequests
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
@ -37,16 +37,14 @@ 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() {
|
override val displayName: String,
|
||||||
|
private val avatarUrl: String?) : ReplacementSpan(), UserMentionSpan {
|
||||||
private val displayName by lazy {
|
|
||||||
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pillDrawable = createChipDrawable()
|
private val pillDrawable = createChipDrawable()
|
||||||
private val target = PillImageSpanTarget(this)
|
private val target = PillImageSpanTarget(this)
|
||||||
|
@ -55,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
||||||
@UiThread
|
@UiThread
|
||||||
fun bind(textView: TextView) {
|
fun bind(textView: TextView) {
|
||||||
tv = WeakReference(textView)
|
tv = WeakReference(textView)
|
||||||
avatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target)
|
avatarRenderer.render(context, glideRequests, avatarUrl, userId, displayName, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplacementSpan *****************************************************************************
|
// ReplacementSpan *****************************************************************************
|
||||||
|
|
|
@ -19,13 +19,11 @@
|
||||||
package im.vector.riotx.features.settings
|
package im.vector.riotx.features.settings
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -38,6 +36,7 @@ import com.bumptech.glide.load.engine.cache.DiskCache
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.showPassword
|
import im.vector.riotx.core.extensions.showPassword
|
||||||
import im.vector.riotx.core.platform.SimpleTextWatcher
|
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||||
|
@ -696,8 +695,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
.setPositiveButton(R.string.settings_change_password_submit, null)
|
.setPositiveButton(R.string.settings_change_password_submit, null)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setOnDismissListener {
|
.setOnDismissListener {
|
||||||
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
view.hideKeyboard()
|
||||||
imm.hideSoftInputFromWindow(view.applicationWindowToken, 0)
|
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
|
@ -762,8 +760,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
showPassword.performClick()
|
showPassword.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
view.hideKeyboard()
|
||||||
imm.hideSoftInputFromWindow(view.applicationWindowToken, 0)
|
|
||||||
|
|
||||||
val oldPwd = oldPasswordText.text.toString().trim()
|
val oldPwd = oldPasswordText.text.toString().trim()
|
||||||
val newPwd = newPasswordText.text.toString().trim()
|
val newPwd = newPasswordText.text.toString().trim()
|
||||||
|
|
Loading…
Reference in New Issue