Add Replies support from within a thread

This commit is contained in:
ariskotsomitopoulos 2021-11-17 13:09:27 +02:00
parent 4160688f83
commit 3d9350091e
6 changed files with 75 additions and 23 deletions

View file

@ -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?
}

View file

@ -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)
// }

View file

@ -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)

View file

@ -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("> <")

View file

@ -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()
}

View file

@ -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)
}
/**