From 6bd7257cf24c24da3443a69e3ed5aa3dc8dfb6dd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 00:14:28 +0100 Subject: [PATCH 01/15] Send mention pills from composer --- .../room/model/relation/RelationService.kt | 6 +- .../api/session/room/send/SendService.kt | 6 +- .../api/session/room/send/UserMentionSpan.kt | 25 ++++++++ .../room/relation/DefaultRelationService.kt | 4 +- .../session/room/send/DefaultSendService.kt | 6 +- .../room/send/LocalEchoEventFactory.kt | 60 ++++++++++++++----- .../riotx/features/command/CommandParser.kt | 4 +- .../riotx/features/command/ParsedCommand.kt | 2 +- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 41 +++++++++++-- .../home/room/detail/RoomDetailViewModel.kt | 12 +++- .../room/detail/composer/TextComposerView.kt | 7 ++- .../riotx/features/html/PillImageSpan.kt | 8 ++- 13 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 5af5183dfa..385699b4db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -72,7 +72,7 @@ interface RelationService { */ fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable @@ -97,12 +97,14 @@ interface RelationService { /** * Reply to an event in the timeline (must be in same room) * 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 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 */ fun replyToMessage(eventReplied: TimelineEvent, - replyText: String, + replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? fun getEventSummaryLive(eventId: String): LiveData> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 8c783837a2..e45069bcff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -29,12 +29,14 @@ interface SendService { /** * 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 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 * @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. @@ -42,7 +44,7 @@ interface SendService { * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt new file mode 100644 index 0000000000..0899e4f27e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 11be821d7e..db3b6100a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv override fun editTextMessage(targetEventId: String, msgType: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { val event = eventFactory @@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv .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) ?.also { saveLocalEcho(it) } ?: return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 7c720e56a7..8fad03b588 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private 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 { saveLocalEcho(it) } @@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private return sendEvent(event) } - override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { - val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also { + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { + val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { saveLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 3fa0dcdca1..4b099a25be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import android.text.SpannableString import androidx.exifinterface.media.ExifInterface import com.zhuinden.monarchy.Monarchy 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.RelationDefaultContent 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.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent @@ -58,37 +60,67 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use // TODO Inject private val renderer = HtmlRenderer.builder().build() - fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { - if (msgType == MessageType.MSGTYPE_TEXT) { - return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown)) + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { + 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) } - private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent { + private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { 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) - if (isFormattedTextPertinent(text, htmlText)) { - return TextContent(text, htmlText) + if (isFormattedTextPertinent(source, htmlText)) { + return TextContent(source, htmlText) + } + } else { + //Try to detect pills + transformPills(text, "%2\$s")?.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?) = text != htmlText && htmlText != "

${text.trim()}

\n" - fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event { - return createEvent(roomId, textContent.toMessageTextContent()) + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { + return createEvent(roomId, textContent.toMessageTextContent(msgType)) } fun createReplaceTextEvent(roomId: String, targetEventId: String, - newBodyText: String, + newBodyText: CharSequence, newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { @@ -279,7 +311,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use 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 // TODO Add error/warning logs when any of this is 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 // - val replyFallback = buildReplyFallback(body, userId, replyText) + val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val eventId = eventReplied.root.eventId ?: return null val content = MessageTextContent( diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 3f5808949b..bc451f8e84 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -27,7 +27,7 @@ object CommandParser { * @param textMessage the text message * @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 if (!textMessage.startsWith("/")) { return ParsedCommand.ErrorNotACommand @@ -76,7 +76,7 @@ object CommandParser { } } 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index 02f5abe540..89438c8a9d 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -33,7 +33,7 @@ sealed class ParsedCommand { // Valid commands: - class SendEmote(val message: String) : ParsedCommand() + class SendEmote(val message: CharSequence) : ParsedCommand() class BanUser(val userId: String, val reason: String) : ParsedCommand() class UnbanUser(val userId: String) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 2e59e70d08..0a6321dd57 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomDetailAction : VectorViewModelAction { 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) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 6fdbf94590..6186bd1ac1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,6 +28,7 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable +import android.text.SpannableStringBuilder import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -609,7 +610,7 @@ class RoomDetailFragment @Inject constructor( attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) } - override fun onSendMessage(text: String) { + override fun onSendMessage(text: CharSequence) { if (lockSendButton) { Timber.w("Send button is locked") return @@ -977,7 +978,9 @@ class RoomDetailFragment @Inject constructor( @SuppressLint("SetTextI18n") 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) { @@ -1166,8 +1169,9 @@ class RoomDetailFragment @Inject constructor( * @param text the text to insert. */ // TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(text: String?) { + private fun insertUserDisplayNameInTextEditor(member: User) { // TODO move logic outside of fragment + val text = member.displayName if (null != text) { // var vibrate = false @@ -1176,19 +1180,44 @@ class RoomDetailFragment @Inject constructor( // current user if (composerLayout.composerEditText.text.isNullOrBlank()) { composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) // vibrate = true } } else { // another user + val sanitizeDisplayName = sanitizeDisplayName(text) if (composerLayout.composerEditText.text.isNullOrBlank()) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { 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 { - 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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index d2c2c7fdde..b8d4ccb7c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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.homeserver.HomeServerCapabilities 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.MessageType 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) } + fun getMember(userId: String) : RoomMember? { + return room.getRoomMember(userId) + } /** * 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) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { - room.editReply(state.sendMode.timelineEvent, it, action.text) + room.editReply(state.sendMode.timelineEvent, it, action.text.toString()) } } else { val messageContent: MessageContent? = @@ -380,7 +384,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro ?: state.sendMode.timelineEvent.root.getClearContent().toModel() 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 val parser = Parser.builder().build() @@ -397,7 +403,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text, action.autoMarkdown) + room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 32307dc3d4..63e74d6f32 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -26,6 +26,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.text.toSpannable import androidx.transition.AutoTransition import androidx.transition.Transition import androidx.transition.TransitionManager @@ -43,7 +44,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib interface Callback : ComposerEditText.Callback { fun onCloseRelatedMessage() - fun onSendMessage(text: String) + fun onSendMessage(text: CharSequence) fun onAddAttachment() } @@ -86,8 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } sendButton.setOnClickListener { - val textMessage = text?.toString() ?: "" - callback?.onSendMessage(textMessage) + val textMessage = text?.toSpannable() + callback?.onSendMessage(textMessage ?: "") } attachmentButton.setOnClickListener { diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index bc954204c0..414cd71de7 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -28,6 +28,7 @@ import androidx.annotation.UiThread import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition 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.riotx.R 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] * 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, private val avatarRenderer: AvatarRenderer, private val context: Context, - private val userId: String, - private val user: User?) : ReplacementSpan() { + override val userId: String, + private val user: User?) : ReplacementSpan(), UserMentionSpan { - private val displayName by lazy { + override val displayName by lazy { if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! } From 2a4cdec0201dd6aa14198bdd40ea3e4dcdf2985f Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 11:40:05 +0100 Subject: [PATCH 02/15] klint cleaning --- .../session/room/send/LocalEchoEventFactory.kt | 13 +++++++------ .../home/room/detail/RoomDetailViewModel.kt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 4b099a25be..3f47b8fff3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -70,7 +70,8 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = transformPills(text,"[%2\$s](https://matrix.to/#/%1\$s)") ?: text.toString() + val source = transformPills(text, "[%2\$s](https://matrix.to/#/%1\$s)") + ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -78,9 +79,9 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use return TextContent(source, htmlText) } } else { - //Try to detect pills + // Try to detect pills transformPills(text, "%2\$s")?.let { - return TextContent(text.toString(),it) + return TextContent(text.toString(), it) } } @@ -88,7 +89,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } private fun transformPills(text: CharSequence, - template : String) + template: String) : String? { val bufSB = StringBuffer() var currIndex = 0 @@ -98,9 +99,9 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use pills.forEachIndexed { _, urlSpan -> val start = it.getSpanStart(urlSpan) val end = it.getSpanEnd(urlSpan) - //We want to replace with the pill with a html link + // 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)) + bufSB.append(String.format(template, urlSpan.userId, urlSpan.displayName)) currIndex = end } bufSB.append(text, currIndex, text.length) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b8d4ccb7c6..a264e0d06c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -386,7 +386,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) - //TODO check for pills? + // TODO check for pills? // TODO Refactor this, just temporary for quotes val parser = Parser.builder().build() From 62bae6708071b9fcb84d23838d1b74657478f296 Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 16 Nov 2019 12:32:50 +0100 Subject: [PATCH 03/15] Code review --- .../api/session/room/send/SendService.kt | 1 + .../api/session/room/send/TextPillsUtils.kt | 67 +++++++++++++++++++ .../room/send/LocalEchoEventFactory.kt | 31 +-------- .../home/room/detail/RoomDetailFragment.kt | 27 +++++--- .../room/detail/composer/TextComposerView.kt | 5 +- .../riotx/features/html/MxLinkTagHandler.kt | 3 +- .../riotx/features/html/PillImageSpan.kt | 10 +-- 7 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index e45069bcff..bdae5eaaa6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -42,6 +42,7 @@ interface SendService { * Method to send a text message with a formatted body. * @param text the text message to send * @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] */ fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt new file mode 100644 index 0000000000..b50d5dd4a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -0,0 +1,67 @@ +/* + * 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 + +import android.text.SpannableString + +/** + * 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...) + */ +object TextPillsUtils { + + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" + + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + + /** + * 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) + ?.takeIf { it.isNotEmpty() } + ?: return null + + return buildString { + var currIndex = 0 + pills.forEachIndexed { _, urlSpan -> + val start = spannableString.getSpanStart(urlSpan) + val end = spannableString.getSpanEnd(urlSpan) + // 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 + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 3f47b8fff3..becba6bffe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever -import android.text.SpannableString import androidx.exifinterface.media.ExifInterface import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R @@ -29,7 +28,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.RelationDefaultContent 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.send.TextPillsUtils 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.internal.database.helper.addSendingEvent @@ -70,7 +69,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = transformPills(text, "[%2\$s](https://matrix.to/#/%1\$s)") + val source = TextPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -80,7 +79,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } } else { // Try to detect pills - transformPills(text, "%2\$s")?.let { + TextPillsUtils.processSpecialSpansToHtml(text)?.let { return TextContent(text.toString(), it) } } @@ -88,30 +87,6 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use 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?) = text != htmlText && htmlText != "

${text.trim()}

\n" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 6186bd1ac1..b6b6270602 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail -import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Context import android.content.DialogInterface @@ -61,6 +60,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho 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.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -406,7 +406,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) 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 { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -419,7 +420,8 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) } } @@ -589,7 +591,8 @@ class RoomDetailFragment @Inject constructor( // Add the span 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) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -976,10 +979,10 @@ class RoomDetailFragment @Inject constructor( vectorBaseActivity.notImplemented("Click on user avatar") } - @SuppressLint("SetTextI18n") override fun onMemberNameClicked(informationData: MessageInformationData) { - session.getUser(informationData.senderId)?.let { - insertUserDisplayNameInTextEditor(it) + val userId = informationData.senderId + roomDetailViewModel.getMember(userId)?.let { + insertUserDisplayNameInTextEditor(userId, it) } } @@ -1169,9 +1172,9 @@ class RoomDetailFragment @Inject constructor( * @param text the text to insert. */ // TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(member: User) { + private fun insertUserDisplayNameInTextEditor(userId: String, memberInfo: RoomMember) { // TODO move logic outside of fragment - val text = member.displayName + val text = memberInfo.displayName if (null != text) { // var vibrate = false @@ -1195,7 +1198,8 @@ class RoomDetailFragment @Inject constructor( SpannableStringBuilder().apply { append(sanitizeDisplayName) setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName + ?: userId, memberInfo.avatarUrl), 0, sanitizeDisplayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE @@ -1208,7 +1212,8 @@ class RoomDetailFragment @Inject constructor( SpannableStringBuilder().apply { append(sanitizeDisplayName) setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), member.userId, member), + PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName + ?: userId, memberInfo.avatarUrl), 0, sanitizeDisplayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 63e74d6f32..f3a29b8dc0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -88,7 +88,10 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib sendButton.setOnClickListener { val textMessage = text?.toSpannable() - callback?.onSendMessage(textMessage ?: "") + callback?.onSendMessage( + textMessage + ?: "" + ) } attachmentButton.setOnClickListener { diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index fdcbb12cd7..ecbf0da415 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -41,7 +41,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, when (permalinkData) { is PermalinkData.UserLink -> { 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( visitor.builder(), span, diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index 414cd71de7..a192c71961 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition 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.riotx.R import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.home.AvatarRenderer @@ -44,11 +43,8 @@ class PillImageSpan(private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, override val userId: String, - private val user: User?) : ReplacementSpan(), UserMentionSpan { - - override val displayName by lazy { - if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! - } + override val displayName: String, + private val avatarUrl: String?) : ReplacementSpan(), UserMentionSpan { private val pillDrawable = createChipDrawable() private val target = PillImageSpanTarget(this) @@ -57,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - avatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target) + avatarRenderer.render(context, glideRequests, avatarUrl, userId, displayName, target) } // ReplacementSpan ***************************************************************************** From 38b93c527b8a2c64df0516970dd03cc0616b100f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 20 Nov 2019 11:14:47 +0100 Subject: [PATCH 04/15] Ensure received pills spans do not overlap --- .../api/session/room/send/TextPillsUtils.kt | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt index b50d5dd4a6..941861f2ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.send import android.text.SpannableString +import java.util.* /** * Utility class to detect special span in CharSequence and turn them into @@ -23,12 +24,17 @@ import android.text.SpannableString * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ + + object TextPillsUtils { + private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" + /** * Detects if transformable spans are present in the text. * @return the transformed String or null if no Span found @@ -49,14 +55,17 @@ object TextPillsUtils { 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 -> - val start = spannableString.getSpanStart(urlSpan) - val end = spannableString.getSpanEnd(urlSpan) + 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)) @@ -64,4 +73,57 @@ object TextPillsUtils { } } } + + private fun pruneOverlaps(links: MutableList) { + Collections.sort(links, COMPARATOR) + 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++ + } + } + + private val COMPARATOR = Comparator { (_, startA, endA), (_, startB, endB) -> + if (startA < startB) { + return@Comparator -1 + } + + if (startA > startB) { + return@Comparator 1 + } + + if (endA < endB) { + return@Comparator 1 + } + + if (endA > endB) { + -1 + } else 0 + } } From 97766404d639d9dce42da317970301009b3a321f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 20 Nov 2019 17:32:36 +0100 Subject: [PATCH 05/15] klint --- .../api/session/room/send/TextPillsUtils.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt index 941861f2ed..936b1d18c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt @@ -24,8 +24,6 @@ import java.util.* * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ - - object TextPillsUtils { private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) @@ -34,7 +32,6 @@ object TextPillsUtils { private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" - /** * Detects if transformable spans are present in the text. * @return the transformed String or null if no Span found @@ -60,7 +57,7 @@ object TextPillsUtils { ?.takeIf { it.isNotEmpty() } ?: return null - //we need to prune overlaps! + // we need to prune overlaps! pruneOverlaps(pills) return buildString { @@ -83,22 +80,20 @@ object TextPillsUtils { val b = links[i + 1] var remove = -1 - //test if there is an overlap + // 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 + // 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 + // 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 + // overlap and a is smaller -> a should be removed remove = i } - if (remove != -1) { links.removeAt(remove) len-- From f984758d37f15ef4688be29b887e86a02a9df1ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:40:02 +0100 Subject: [PATCH 06/15] Pills: Daggerization --- .../room/send/LocalEchoEventFactory.kt | 15 ++++---- .../room/send/pills/MentionLinkSpec.kt | 25 ++++++++++++++ .../send/pills/MentionLinkSpecComparator.kt | 32 +++++++++++++++++ .../room/send/pills}/TextPillsUtils.kt | 34 ++++++------------- 4 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api/session/room/send => internal/session/room/send/pills}/TextPillsUtils.kt (81%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index becba6bffe..b773d1f892 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -28,7 +28,6 @@ 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.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent -import im.vector.matrix.android.api.session.room.send.TextPillsUtils 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.internal.database.helper.addSendingEvent @@ -37,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.session.content.ThumbnailExtractor 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 org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -51,9 +51,12 @@ import javax.inject.Inject * * The transactionID is used as loc */ -internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String, - private val stringProvider: StringProvider, - private val roomSummaryUpdater: RoomSummaryUpdater) { +internal class LocalEchoEventFactory @Inject constructor( + @UserId private val userId: String, + private val stringProvider: StringProvider, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val textPillsUtils: TextPillsUtils +) { // TODO Inject private val parser = Parser.builder().build() // TODO Inject @@ -69,7 +72,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = TextPillsUtils.processSpecialSpansToMarkdown(text) + val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() val document = parser.parse(source) val htmlText = renderer.render(document) @@ -79,7 +82,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use } } else { // Try to detect pills - TextPillsUtils.processSpecialSpansToHtml(text)?.let { + textPillsUtils.processSpecialSpansToHtml(text)?.let { return TextContent(text.toString(), it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt new file mode 100644 index 0000000000..5ad61b5441 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpec.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt new file mode 100644 index 0000000000..76fd8336cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/MentionLinkSpecComparator.kt @@ -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 { + + 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 + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt similarity index 81% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt index 936b1d18c2..02f48e5800 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/TextPillsUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.api.session.room.send +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 @@ -24,13 +26,9 @@ import java.util.* * * For now only support UserMentionSpans (TODO rooms, room aliases, etc...) */ -object TextPillsUtils { - - private data class MentionLinkSpec(val span: UserMentionSpan, val start: Int, val end: Int) - - private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" - - private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" +internal class TextPillsUtils @Inject constructor( + private val mentionLinkSpecComparator: MentionLinkSpecComparator +) { /** * Detects if transformable spans are present in the text. @@ -72,7 +70,7 @@ object TextPillsUtils { } private fun pruneOverlaps(links: MutableList) { - Collections.sort(links, COMPARATOR) + Collections.sort(links, mentionLinkSpecComparator) var len = links.size var i = 0 while (i < len - 1) { @@ -104,21 +102,9 @@ object TextPillsUtils { } } - private val COMPARATOR = Comparator { (_, startA, endA), (_, startB, endB) -> - if (startA < startB) { - return@Comparator -1 - } + companion object { + private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s" - if (startA > startB) { - return@Comparator 1 - } - - if (endA < endB) { - return@Comparator 1 - } - - if (endA > endB) { - -1 - } else 0 + private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)" } } From f11cd47df3e036e5969e01ac27595b482a951727 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:40:44 +0100 Subject: [PATCH 07/15] Pills: cleanup --- .../android/api/session/room/send/UserMentionSpan.kt | 5 +++-- .../features/home/room/detail/composer/TextComposerView.kt | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt index 0899e4f27e..4cd8080dc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package im.vector.matrix.android.api.session.room.send /** @@ -20,6 +21,6 @@ package im.vector.matrix.android.api.session.room.send * These Spans will be transformed into pills when detected in message to send */ interface UserMentionSpan { - abstract val displayName: String - abstract val userId: String + val displayName: String + val userId: String } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index f3a29b8dc0..593ce1a8f6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -87,11 +87,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } sendButton.setOnClickListener { - val textMessage = text?.toSpannable() - callback?.onSendMessage( - textMessage - ?: "" - ) + val textMessage = text?.toSpannable() ?: "" + callback?.onSendMessage(textMessage) } attachmentButton.setOnClickListener { From 4b273e8746500010fec7f730a811ad6a68b8526a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 21:54:37 +0100 Subject: [PATCH 08/15] Pills: simplify and improve the algorithm --- .../home/room/detail/RoomDetailFragment.kt | 140 ++++++++---------- 1 file changed, 62 insertions(+), 78 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index b6b6270602..baf24a51a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail +import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Context import android.content.DialogInterface @@ -27,7 +28,6 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable -import android.text.SpannableStringBuilder import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -37,6 +37,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.text.buildSpannedString import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach @@ -60,7 +61,6 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho 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.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -159,7 +159,7 @@ class RoomDetailFragment @Inject constructor( companion object { - /**x + /** * Sanitize the display name. * * @param displayName the display name to sanitize @@ -406,8 +406,12 @@ class RoomDetailFragment @Inject constructor( composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) 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 { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -420,8 +424,7 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) } } @@ -591,8 +594,13 @@ class RoomDetailFragment @Inject constructor( // Add the span val user = session.getUser(item.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, requireContext(), item.userId, user?.displayName - ?: item.userId, user?.avatarUrl) + val span = PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + item.userId, + user?.displayName ?: item.userId, + user?.avatarUrl) span.bind(composerLayout.composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -980,10 +988,7 @@ class RoomDetailFragment @Inject constructor( } override fun onMemberNameClicked(informationData: MessageInformationData) { - val userId = informationData.senderId - roomDetailViewModel.getMember(userId)?.let { - insertUserDisplayNameInTextEditor(userId, it) - } + insertUserDisplayNameInTextEditor(informationData.senderId) } override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { @@ -1165,77 +1170,56 @@ 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 - private fun insertUserDisplayNameInTextEditor(userId: String, memberInfo: RoomMember) { - // TODO move logic outside of fragment - val text = memberInfo.displayName - if (null != text) { -// var vibrate = false + @SuppressLint("SetTextI18n") + private fun insertUserDisplayNameInTextEditor(userId: String) { + val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() - val myDisplayName = session.getUser(session.myUserId)?.displayName - if (myDisplayName == text) { - // current user - if (composerLayout.composerEditText.text.isNullOrBlank()) { - composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) -// vibrate = true - } - } else { - // another user - val sanitizeDisplayName = sanitizeDisplayName(text) - if (composerLayout.composerEditText.text.isNullOrBlank()) { - // Ensure displayName will not be interpreted as a Slash command - if (text.startsWith("/")) { - composerLayout.composerEditText.append("\\") + if (startToCompose + && userId == session.myUserId) { + // Empty composer, current user: start an emote + composerLayout.composerEditText.setText(Command.EMOTE.command + " ") + composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1) + } else { + val roomMember = roomDetailViewModel.getMember(userId) + // TODO move logic outside of fragment + (roomMember?.displayName ?: userId) + .let { sanitizeDisplayName(it) } + .let { displayName -> + buildSpannedString { + append(displayName) + setSpan( + PillImageSpan( + glideRequests, + avatarRenderer, + requireContext(), + userId, + displayName, + roomMember?.avatarUrl), + 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) + } + } } - SpannableStringBuilder().apply { - append(sanitizeDisplayName) - setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName - ?: userId, memberInfo.avatarUrl), - 0, - sanitizeDisplayName.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - append(": ") - }.let { - composerLayout.composerEditText.append(it) - } - } else { - SpannableStringBuilder().apply { - append(sanitizeDisplayName) - setSpan( - PillImageSpan(glideRequests, avatarRenderer, requireContext(), userId, memberInfo.displayName - ?: userId, memberInfo.avatarUrl), - 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 - } - -// 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() { From a3f8f138a61f3c37a72afcddaec918ba715e83a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 28 Nov 2019 22:05:59 +0100 Subject: [PATCH 09/15] Create showKeyBoard() extension --- .../java/im/vector/riotx/core/extensions/View.kt | 12 ++++++++++-- .../CreateDirectRoomDirectoryUsersFragment.kt | 5 ++--- .../features/home/room/detail/RoomDetailFragment.kt | 5 ++--- .../settings/VectorSettingsGeneralFragment.kt | 7 +++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt index bcbab97360..41f98ed264 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/View.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/View.kt @@ -21,6 +21,14 @@ import android.view.View import android.view.inputmethod.InputMethodManager fun View.hideKeyboard() { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index cf6abf12e9..17eef126d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch +import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject @@ -63,9 +64,7 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) } .disposeOnDestroyView() - createDirectRoomSearchById.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT) + createDirectRoomSearchById.showKeyboard(andRequestFocus = true) } private fun setupCloseView() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index baf24a51a6..fc7613c530 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -74,6 +74,7 @@ import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent 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.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment @@ -1223,9 +1224,7 @@ class RoomDetailFragment @Inject constructor( } private fun focusComposerAndShowKeyboard() { - composerLayout.composerEditText.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) + composerLayout.composerEditText.showKeyboard(andRequestFocus = true) } private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index ff76c61754..cbf2f0eec1 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -38,6 +38,7 @@ import com.bumptech.glide.load.engine.cache.DiskCache import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.UserAvatarPreference @@ -696,8 +697,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { .setPositiveButton(R.string.settings_change_password_submit, null) .setNegativeButton(R.string.cancel, null) .setOnDismissListener { - val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + view.hideKeyboard() } .create() @@ -762,8 +762,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { showPassword.performClick() } - val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + view.hideKeyboard() val oldPwd = oldPasswordText.text.toString().trim() val newPwd = newPasswordText.text.toString().trim() From 5d3c376267702b2fbacc77ad8ec11e5823a627fb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 10:29:40 +0100 Subject: [PATCH 10/15] Pills: remove pills when a char is deleted --- .../room/detail/composer/ComposerEditText.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index 273aeecbfa..093792ca17 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -20,12 +20,16 @@ package im.vector.riotx.features.home.room.detail.composer import android.content.Context import android.net.Uri import android.os.Build +import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat +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) : AppCompatEditText(context, attrs, defStyleAttr) { @@ -55,4 +59,38 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib } 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("beforeTextChanged: start:$start count:$count after:$after") + + if (count > after) { + // A char has been deleted + val deleteCharPosition = start + count + Timber.v("beforeTextChanged: deleted char at $deleteCharPosition") + + // Get span at this position + val spans = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) + spanToRemove = spans.firstOrNull() + } + } + + override fun afterTextChanged(s: Editable) { + if (spanToRemove != null) { + Timber.v("Removing the span") + val start = editableText.getSpanStart(spanToRemove) + val end = editableText.getSpanEnd(spanToRemove) + // Must be done before text replacement + editableText.removeSpan(spanToRemove) + editableText.replace(start, end, "") + spanToRemove = null + } + } + } + ) + } } From c412006f0eb430f61c8f6714e0586fe1d52d74d1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:13:23 +0100 Subject: [PATCH 11/15] Pills: render the avatar --- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fc7613c530..59437273d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail import android.annotation.SuppressLint import android.app.Activity.RESULT_OK -import android.content.Context import android.content.DialogInterface import android.content.Intent import android.graphics.drawable.ColorDrawable @@ -29,7 +28,6 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.view.* -import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes @@ -1200,7 +1198,8 @@ class RoomDetailFragment @Inject constructor( requireContext(), userId, displayName, - roomMember?.avatarUrl), + roomMember?.avatarUrl) + .also { it.bind(composerLayout.composerEditText) }, 0, displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE From 9f9c41808504cfecfec8caef4b21e67564ff4acf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:17:11 +0100 Subject: [PATCH 12/15] Pills: cleanup and robustness --- .../room/detail/composer/ComposerEditText.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index 093792ca17..ce27b1c098 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -66,27 +66,30 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib var spanToRemove: PillImageSpan? = null override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - Timber.v("beforeTextChanged: start:$start count:$count after:$after") + Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after") if (count > after) { // A char has been deleted val deleteCharPosition = start + count - Timber.v("beforeTextChanged: deleted char at $deleteCharPosition") + Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition") - // Get span at this position - val spans = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) - spanToRemove = spans.firstOrNull() + // Get the first span at this position + spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) + .also { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } + .firstOrNull() } } override fun afterTextChanged(s: Editable) { if (spanToRemove != null) { - Timber.v("Removing the span") 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) - editableText.replace(start, end, "") + if (start != -1 && end != -1) { + editableText.replace(start, end, "") + } spanToRemove = null } } From 46d96429e08c53b6c5d6fbf3c7475831fe3fd3ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:20:46 +0100 Subject: [PATCH 13/15] Create ooi extension --- .../java/im/vector/riotx/core/extensions/BasicExtensions.kt | 2 ++ .../features/home/room/detail/composer/ComposerEditText.kt | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt index 2dc75c5fa2..1e3da7f878 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt @@ -21,6 +21,8 @@ import androidx.fragment.app.Fragment fun Boolean.toOnOff() = if (this) "ON" else "OFF" +inline fun T.ooi(block: (T) -> Unit): T = also(block) + /** * Apply argument to a Fragment */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt index ce27b1c098..ab37431103 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/ComposerEditText.kt @@ -27,6 +27,7 @@ import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat 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 @@ -75,7 +76,7 @@ class ComposerEditText @JvmOverloads constructor(context: Context, attrs: Attrib // Get the first span at this position spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) - .also { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } + .ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } .firstOrNull() } } From 10cc270273381b7df7d7bc0c85e0f4f1813e7569 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 29 Nov 2019 11:26:19 +0100 Subject: [PATCH 14/15] ktlint --- .../home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt | 2 -- .../riotx/features/settings/VectorSettingsGeneralFragment.kt | 2 -- 2 files changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index 17eef126d8..59f31ec2ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -16,10 +16,8 @@ package im.vector.riotx.features.home.createdirect -import android.content.Context import android.os.Bundle import android.view.View -import android.view.inputmethod.InputMethodManager import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index cbf2f0eec1..ca994db62c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -19,13 +19,11 @@ package im.vector.riotx.features.settings import android.app.Activity -import android.content.Context import android.content.Intent import android.text.Editable import android.util.Patterns import android.view.View import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat From 67fe776d910d035c5e9939f44961bf0c6e7f4379 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 15 Nov 2019 10:12:23 +0100 Subject: [PATCH 15/15] Update Changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0f1c76203d..27619f7d28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Send mention Pills from composer Other changes: - Fix a small grammatical error when an empty room list is shown.