From 3d9350091ef44e6284a911c380f43679fd29265f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 17 Nov 2021 13:09:27 +0200 Subject: [PATCH] Add Replies support from within a thread --- .../room/model/relation/RelationService.kt | 6 +++- .../room/relation/DefaultRelationService.kt | 27 +++++++++++---- .../session/room/relation/EventEditor.kt | 6 +++- .../room/send/LocalEchoEventFactory.kt | 23 ++++++++++--- .../detail/composer/TextComposerAction.kt | 2 -- .../detail/composer/TextComposerViewModel.kt | 34 ++++++++++++++----- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index be6d1d9aa3..a5ecfaf6e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -134,11 +134,15 @@ interface RelationService { * by the sdk into pills. * @param rootThreadEventId the root thread eventId * @param replyInThreadText the reply text + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param eventReplied the event referenced by the reply within a thread */ fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false, - formattedText: String? = null): Cancelable? + formattedText: String? = null, + eventReplied: TimelineEvent? = null): Cancelable? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 2184e83ac5..23862ae963 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -38,7 +38,6 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.session.room.send.TextContent import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -133,7 +132,11 @@ internal class DefaultRelationService @AssistedInject constructor( } override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { - val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) + val event = eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = eventReplied, + replyText = replyText, + autoMarkdown = autoMarkdown) ?.also { saveLocalEcho(it) } ?: return null @@ -159,15 +162,27 @@ internal class DefaultRelationService @AssistedInject constructor( } } - override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String, autoMarkdown: Boolean, formattedText: String?): Cancelable { - val event = eventFactory.createThreadTextEvent( + override fun replyInThread( + rootThreadEventId: String, + replyInThreadText: CharSequence, + msgType: String, + autoMarkdown: Boolean, + formattedText: String?, + eventReplied: TimelineEvent?): Cancelable { + val event = eventReplied?.let { + eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = eventReplied, + replyText = replyInThreadText, + autoMarkdown = autoMarkdown, + rootThreadEventId = rootThreadEventId) + } ?: eventFactory.createThreadTextEvent( rootThreadEventId = rootThreadEventId, roomId = roomId, text = replyInThreadText.toString(), msgType = msgType, autoMarkdown = autoMarkdown, - formattedText = formattedText - ) + formattedText = formattedText) // .also { // saveLocalEcho(it) // } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index a666d40fc3..7e3d7dfde8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -67,7 +67,11 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: val roomId = replyToEdit.roomId if (replyToEdit.root.sendState.hasFailed()) { // We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event. - val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy( + val editedEvent = eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = originalTimelineEvent, + replyText = newBodyText, + autoMarkdown = false)?.copy( eventId = replyToEdit.eventId ) ?: return NoOpCancellable updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 7d99dc67bf..5741d0f5ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -341,7 +341,7 @@ internal class LocalEchoEventFactory @Inject constructor( } /** - * Creates a thread event related to the already existing event + * Creates a thread event related to the already existing root event */ fun createThreadTextEvent( rootThreadEventId: String, @@ -363,10 +363,14 @@ internal class LocalEchoEventFactory @Inject constructor( return System.currentTimeMillis() } + /** + * Creates a reply to a regular timeline Event or a thread Event if needed + */ fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, - autoMarkdown: Boolean): Event? { + autoMarkdown: Boolean, + rootThreadEventId: String? = null): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null @@ -393,11 +397,22 @@ internal class LocalEchoEventFactory @Inject constructor( format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted, - relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) - ) + relatesTo = generateReplyRelationContent(eventId = eventId, rootThreadEventId = rootThreadEventId)) return createMessageEvent(roomId, content) } + /** + * Generates the appropriate relatesTo object for a reply event. + * It can either be a regular reply or a reply within a thread + */ + private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null): RelationDefaultContent = + rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId)) + } ?: RelationDefaultContent(null, null, ReplyToContent(eventId)) + private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { return buildString { append("> <") diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt index 48f6c84983..7725400187 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -28,6 +28,4 @@ sealed class TextComposerAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() data class OnTextChanged(val text: CharSequence) : TextComposerAction() data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction() - data class EnterReplyInThreadTimeline(val rootThreadEventId: String) : TextComposerAction() - } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 1fa1bfde35..bcc26247a2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -276,7 +276,7 @@ class TextComposerViewModel @AssistedInject constructor( replyInThreadText = slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, formattedText = rainbowGenerator.generate(message)) - } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message),MessageType.MSGTYPE_EMOTE) + } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() @@ -465,20 +465,36 @@ class TextComposerViewModel @AssistedInject constructor( val document = parser.parse(finalText) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) + if (finalText == htmlText) { - room.sendTextMessage(finalText) + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = it, + replyInThreadText = finalText) + } ?: room.sendTextMessage(finalText) } else { - room.sendFormattedTextMessage(finalText, htmlText) + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = it, + replyInThreadText = finalText, + formattedText = htmlText) + } ?: room.sendFormattedTextMessage(finalText, htmlText) } _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } is SendMode.REPLY -> { - state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text.toString(), action.autoMarkdown) - _viewEvents.post(TextComposerViewEvents.MessageSent) - popDraft() - } + val timelineEvent = state.sendMode.timelineEvent + state.rootThreadEventId?.let { rootThreadEventId -> + room.replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = action.text.toString(), + autoMarkdown = action.autoMarkdown, + eventReplied = timelineEvent) + } ?: room.replyToMessage(timelineEvent, action.text.toString(), action.autoMarkdown) + + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() } }.exhaustive } @@ -705,7 +721,7 @@ class TextComposerViewModel @AssistedInject constructor( } rootThreadEventId?.let { room.replyInThread(it, sequence) - }?: room.sendTextMessage(sequence) + } ?: room.sendTextMessage(sequence) } /**