Add is_falling_back support for rich thread replies

Enhance thread awareness handler so normal replies with thread disabled will be visible in te appropriate thread
Fix conflicts
This commit is contained in:
ariskotsomitopoulos 2022-03-10 17:51:02 +02:00
parent 21111922e6
commit a758ad71e6
13 changed files with 91 additions and 59 deletions

View file

@ -389,7 +389,7 @@ fun Event.isReply(): Boolean {
} }
fun Event.isReplyRenderedInThread(): Boolean { fun Event.isReplyRenderedInThread(): Boolean {
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true return isReply() && getRelationContent()?.shouldRenderInThread() == true
} }
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null

View file

@ -26,5 +26,6 @@ data class ReactionInfo(
@Json(name = "key") val key: String, @Json(name = "key") val key: String,
// always null for reaction // always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null @Json(name = "option") override val option: Int? = null,
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
) : RelationContent ) : RelationContent

View file

@ -24,4 +24,5 @@ interface RelationContent {
val eventId: String? val eventId: String?
val inReplyTo: ReplyToContent? val inReplyTo: ReplyToContent?
val option: Int? val option: Int?
val isFallingBack: Boolean? // Thread fallback to differentiate replies within threads
} }

View file

@ -23,5 +23,8 @@ data class RelationDefaultContent(
@Json(name = "rel_type") override val type: String?, @Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String?, @Json(name = "event_id") override val eventId: String?,
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null @Json(name = "option") override val option: Int? = null,
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
) : RelationContent ) : RelationContent
fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false

View file

@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ReplyToContent( data class ReplyToContent(
@Json(name = "event_id") val eventId: String? = null, @Json(name = "event_id") val eventId: String? = null
@Json(name = "render_in") val renderIn: List<String>? = null
) )
fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true

View file

@ -127,7 +127,7 @@ private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<
return timelineEventEntity return timelineEventEntity
} }
internal fun ThreadSummaryEntity.Companion.createOrUpdate( internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
threadSummaryType: ThreadSummaryUpdateType, threadSummaryType: ThreadSummaryUpdateType,
realm: Realm, realm: Realm,
roomId: String, roomId: String,
@ -204,7 +204,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
} }
} }
private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) { private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
cryptoService ?: return cryptoService ?: return
val event = eventEntity.asDomain() val event = eventEntity.asDomain()
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) { if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {

View file

@ -172,7 +172,7 @@ internal class DefaultRelationService @AssistedInject constructor(
replyText = replyInThreadText, replyText = replyInThreadText,
autoMarkdown = autoMarkdown, autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
showInThread = true showInThread = false
) )
?.also { ?.also {
saveLocalEcho(it) saveLocalEcho(it)

View file

@ -100,7 +100,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
eventReplied = originalTimelineEvent, eventReplied = originalTimelineEvent,
replyText = newBodyText, replyText = newBodyText,
autoMarkdown = false, autoMarkdown = false,
showInThread = false showInThread = false // Test that value
)?.copy( )?.copy(
eventId = replyToEdit.eventId eventId = replyToEdit.eventId
) ?: return NoOpCancellable ) ?: return NoOpCancellable

View file

@ -560,7 +560,7 @@ internal class LocalEchoEventFactory @Inject constructor(
relatesTo = generateReplyRelationContent( relatesTo = generateReplyRelationContent(
eventId = eventId, eventId = eventId,
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
showAsReply = showInThread)) showInThread = showInThread))
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content)
} }
@ -570,18 +570,20 @@ internal class LocalEchoEventFactory @Inject constructor(
* "m.relates_to": { * "m.relates_to": {
* "rel_type": "m.thread", * "rel_type": "m.thread",
* "event_id": "$thread_root", * "event_id": "$thread_root",
* "is_falling_back": false,
* "m.in_reply_to": { * "m.in_reply_to": {
* "event_id": "$event_target", * "event_id": "$event_target"
* "render_in": ["m.thread"] * }
* } * }
* }
*/ */
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent = private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
rootThreadEventId?.let { rootThreadEventId?.let {
RelationDefaultContent( RelationDefaultContent(
type = RelationType.THREAD, type = RelationType.THREAD,
eventId = it, eventId = it,
inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null)) isFallingBack = showInThread,
// False when is a rich reply from within a thread, and true when is a reply that should be visible from threads
inReplyTo = ReplyToContent(eventId = eventId))
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId)) } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String { private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {

View file

@ -60,6 +60,7 @@ fun TextContent.toThreadTextContent(
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
type = RelationType.THREAD, type = RelationType.THREAD,
eventId = rootThreadEventId, eventId = rootThreadEventId,
isFallingBack = true,
inReplyTo = ReplyToContent( inReplyTo = ReplyToContent(
eventId = latestThreadEventId eventId = latestThreadEventId
)), )),

View file

@ -102,11 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy() data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
} }
fun handle(realm: Realm, suspend fun handle(realm: Realm,
roomsSyncResponse: RoomsSyncResponse, roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean, isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator, aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter? = null) { reporter: ProgressReporter? = null) {
Timber.v("Execute transaction from $this") Timber.v("Execute transaction from $this")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
@ -121,11 +121,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleRoomSync(realm: Realm, private suspend fun handleRoomSync(realm: Realm,
handlingStrategy: HandlingStrategy, handlingStrategy: HandlingStrategy,
isInitialSync: Boolean, isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator, aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) { reporter: ProgressReporter?) {
val insertType = if (isInitialSync) { val insertType = if (isInitialSync) {
EventInsertType.INITIAL_SYNC EventInsertType.INITIAL_SYNC
} else { } else {
@ -158,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
realm.insertOrUpdate(rooms) realm.insertOrUpdate(rooms)
} }
private fun insertJoinRoomsFromInitSync(realm: Realm, private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
handlingStrategy: HandlingStrategy.JOINED, handlingStrategy: HandlingStrategy.JOINED,
syncLocalTimeStampMillis: Long, syncLocalTimeStampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator, aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) { reporter: ProgressReporter?) {
val bestChunkSize = computeBestChunkSize( val bestChunkSize = computeBestChunkSize(
listSize = handlingStrategy.data.keys.size, listSize = handlingStrategy.data.keys.size,
limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
@ -200,12 +200,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
} }
private fun handleJoinedRoom(realm: Realm, private suspend fun handleJoinedRoom(realm: Realm,
roomId: String, roomId: String,
roomSync: RoomSync, roomSync: RoomSync,
insertType: EventInsertType, insertType: EventInsertType,
syncLocalTimestampMillis: Long, syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity { aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
Timber.v("Handle join sync for room $roomId") Timber.v("Handle join sync for room $roomId")
val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed) val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
@ -351,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return roomEntity return roomEntity
} }
private fun handleTimelineEvents(realm: Realm, private suspend fun handleTimelineEvents(realm: Realm,
roomId: String, roomId: String,
roomEntity: RoomEntity, roomEntity: RoomEntity,
eventList: List<Event>, eventList: List<Event>,
prevToken: String? = null, prevToken: String? = null,
isLimited: Boolean = true, isLimited: Boolean = true,
insertType: EventInsertType, insertType: EventInsertType,
syncLocalTimestampMillis: Long, syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
if (isLimited && lastChunk != null) { if (isLimited && lastChunk != null) {
lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true) lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)

View file

@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
} }
val eventBody = event.getDecryptedTextSummary() ?: return null val eventBody = event.getDecryptedTextSummary() ?: return null
val threadRelation = getRootThreadRelationContent(event)
val eventIdToInject = getPreviousEventOrRoot(event) ?: run { val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
} }
val eventToInject = getEventFromDB(realm, eventIdToInject) val eventToInject = getEventFromDB(realm, eventIdToInject)
val eventToInjectBody = eventToInject?.getDecryptedTextSummary() val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId, roomId = roomId,
eventBody = eventBody, eventBody = eventBody,
eventToInject = eventToInject, eventToInject = eventToInject,
eventToInjectBody = eventToInjectBody) ?: return null eventToInjectBody = eventToInjectBody,
threadRelation = threadRelation) ?: return null
// update the event // update the event
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent) contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
} else { } else {
contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
} }
// Now lets try to find relations for improved results, while some events may come with reverse order // Now lets try to find relations for improved results, while some events may come with reverse order
eventEntity?.let { eventEntity?.let {
// When eventEntity is not null means that we are not from within roomSyncHandler // When eventEntity is not null means that we are not from within roomSyncHandler
handleEventsThatRelatesTo(realm, roomId, event, eventBody, false) handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation)
} }
return contentForNonEncrypted return contentForNonEncrypted
} }
@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param event the current event received * @param event the current event received
* @return The content to inject in the roomSyncHandler live events * @return The content to inject in the roomSyncHandler live events
*/ */
private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? { private fun handleRootThreadEventsIfNeeded(
realm: Realm,
roomId: String,
eventEntity: EventEntity?,
event: Event
): String? {
if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) { if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
eventEntity?.let { eventEntity?.let {
val eventBody = event.getDecryptedTextSummary() ?: return null val eventBody = event.getDecryptedTextSummary() ?: return null
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true) return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null)
} }
} }
return null return null
@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param isFromCache determines whether or not we already know this is root thread event * @param isFromCache determines whether or not we already know this is root thread event
* @return The content to inject in the roomSyncHandler live events * @return The content to inject in the roomSyncHandler live events
*/ */
private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? { private fun handleEventsThatRelatesTo(
realm: Realm,
roomId: String,
event: Event,
eventBody: String,
isFromCache: Boolean,
threadRelation: RelationDefaultContent?
): String? {
event.eventId ?: return null event.eventId ?: return null
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound -> eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId, roomId = roomId,
eventBody = newEventBody, eventBody = newEventBody,
eventToInject = event, eventToInject = event,
eventToInjectBody = eventBody) ?: return null eventToInjectBody = eventBody,
threadRelation = threadRelation) ?: return null
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent) return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
} }
@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectEvent(roomId: String, private fun injectEvent(roomId: String,
eventBody: String, eventBody: String,
eventToInject: Event, eventToInject: Event,
eventToInjectBody: String): Content? { eventToInjectBody: String,
threadRelation: RelationDefaultContent?
): Content? {
val eventToInjectId = eventToInject.eventId ?: return null val eventToInjectId = eventToInject.eventId ?: return null
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty() val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false) val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
eventBody) eventBody)
return MessageTextContent( return MessageTextContent(
relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT, msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML, format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody, body = eventBody,
@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectFallbackIndicator(event: Event, private fun injectFallbackIndicator(event: Event,
eventBody: String, eventBody: String,
eventEntity: EventEntity?, eventEntity: EventEntity?,
eventPayload: MutableMap<String, Any>): String? { eventPayload: MutableMap<String, Any>,
threadRelation: RelationDefaultContent?): String? {
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format( val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
"In reply to a thread", "In reply to a thread",
eventBody) eventBody)
val messageTextContent = MessageTextContent( val messageTextContent = MessageTextContent(
relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT, msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML, format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody, body = eventBody,
@ -359,6 +381,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun getRootThreadEventId(event: Event): String? = private fun getRootThreadEventId(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
event.content.toModel<MessageRelationContent>()?.relatesTo
private fun getPreviousEventOrRoot(event: Event): String? = private fun getPreviousEventOrRoot(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId

View file

@ -465,7 +465,8 @@ class MessageComposerViewModel @AssistedInject constructor(
// is original event a reply? // is original event a reply?
val relationContent = state.sendMode.timelineEvent.getRelationContent() val relationContent = state.sendMode.timelineEvent.getRelationContent()
val inReplyTo = if (state.rootThreadEventId != null) { val inReplyTo = if (state.rootThreadEventId != null) {
if (relationContent?.inReplyTo?.shouldRenderInThread() == true) { // Thread event
if (relationContent?.shouldRenderInThread() == true) {
// Reply within a thread event // Reply within a thread event
relationContent.inReplyTo?.eventId relationContent.inReplyTo?.eventId
} else { } else {
@ -509,6 +510,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is SendMode.Reply -> { is SendMode.Reply -> {
val timelineEvent = state.sendMode.timelineEvent val timelineEvent = state.sendMode.timelineEvent
val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null
// If threads are disabled this will make the fallback replies visible to clients with threads enabled
val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null
state.rootThreadEventId?.let { state.rootThreadEventId?.let {
room.replyInThread( room.replyInThread(