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 {
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
return isReply() && getRelationContent()?.shouldRenderInThread() == true
}
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null

View file

@ -26,5 +26,6 @@ data class ReactionInfo(
@Json(name = "key") val key: String,
// always null for reaction
@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

View file

@ -24,4 +24,5 @@ interface RelationContent {
val eventId: String?
val inReplyTo: ReplyToContent?
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 = "event_id") override val eventId: String?,
@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
fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false

View file

@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReplyToContent(
@Json(name = "event_id") val eventId: String? = null,
@Json(name = "render_in") val renderIn: List<String>? = null
@Json(name = "event_id") val eventId: 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
}
internal fun ThreadSummaryEntity.Companion.createOrUpdate(
internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
threadSummaryType: ThreadSummaryUpdateType,
realm: Realm,
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
val event = eventEntity.asDomain()
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {

View file

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

View file

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

View file

@ -560,7 +560,7 @@ internal class LocalEchoEventFactory @Inject constructor(
relatesTo = generateReplyRelationContent(
eventId = eventId,
rootThreadEventId = rootThreadEventId,
showAsReply = showInThread))
showInThread = showInThread))
return createMessageEvent(roomId, content)
}
@ -570,18 +570,20 @@ internal class LocalEchoEventFactory @Inject constructor(
* "m.relates_to": {
* "rel_type": "m.thread",
* "event_id": "$thread_root",
* "is_falling_back": false,
* "m.in_reply_to": {
* "event_id": "$event_target",
* "render_in": ["m.thread"]
* }
* }
* "event_id": "$event_target"
* }
* }
*/
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
rootThreadEventId?.let {
RelationDefaultContent(
type = RelationType.THREAD,
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))
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(
type = RelationType.THREAD,
eventId = rootThreadEventId,
isFallingBack = true,
inReplyTo = ReplyToContent(
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()
}
fun handle(realm: Realm,
roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter? = null) {
suspend fun handle(realm: Realm,
roomsSyncResponse: RoomsSyncResponse,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter? = null) {
Timber.v("Execute transaction from $this")
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), 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 fun handleRoomSync(realm: Realm,
handlingStrategy: HandlingStrategy,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
private suspend fun handleRoomSync(realm: Realm,
handlingStrategy: HandlingStrategy,
isInitialSync: Boolean,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
val insertType = if (isInitialSync) {
EventInsertType.INITIAL_SYNC
} else {
@ -158,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
realm.insertOrUpdate(rooms)
}
private fun insertJoinRoomsFromInitSync(realm: Realm,
handlingStrategy: HandlingStrategy.JOINED,
syncLocalTimeStampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
handlingStrategy: HandlingStrategy.JOINED,
syncLocalTimeStampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator,
reporter: ProgressReporter?) {
val bestChunkSize = computeBestChunkSize(
listSize = handlingStrategy.data.keys.size,
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,
roomId: String,
roomSync: RoomSync,
insertType: EventInsertType,
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
private suspend fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync,
insertType: EventInsertType,
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
Timber.v("Handle join sync for room $roomId")
val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
@ -351,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return roomEntity
}
private fun handleTimelineEvents(realm: Realm,
roomId: String,
roomEntity: RoomEntity,
eventList: List<Event>,
prevToken: String? = null,
isLimited: Boolean = true,
insertType: EventInsertType,
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
private suspend fun handleTimelineEvents(realm: Realm,
roomId: String,
roomEntity: RoomEntity,
eventList: List<Event>,
prevToken: String? = null,
isLimited: Boolean = true,
insertType: EventInsertType,
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
if (isLimited && lastChunk != null) {
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.MessageTextContent
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.sync.model.SyncResponse
import org.matrix.android.sdk.api.util.JsonDict
@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
}
val eventBody = event.getDecryptedTextSummary() ?: return null
val threadRelation = getRootThreadRelationContent(event)
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 eventToInjectBody = eventToInject?.getDecryptedTextSummary()
@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId,
eventBody = eventBody,
eventToInject = eventToInject,
eventToInjectBody = eventToInjectBody) ?: return null
eventToInjectBody = eventToInjectBody,
threadRelation = threadRelation) ?: return null
// update the event
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
} 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
eventEntity?.let {
// 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
}
@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
* @param event the current event received
* @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)) {
eventEntity?.let {
val eventBody = event.getDecryptedTextSummary() ?: return null
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, 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
* @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
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
roomId = roomId,
eventBody = newEventBody,
eventToInject = event,
eventToInjectBody = eventBody) ?: return null
eventToInjectBody = eventBody,
threadRelation = threadRelation) ?: return null
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
}
@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectEvent(roomId: String,
eventBody: String,
eventToInject: Event,
eventToInjectBody: String): Content? {
eventToInjectBody: String,
threadRelation: RelationDefaultContent?
): Content? {
val eventToInjectId = eventToInject.eventId ?: return null
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
eventBody)
return MessageTextContent(
relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody,
@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun injectFallbackIndicator(event: Event,
eventBody: String,
eventEntity: EventEntity?,
eventPayload: MutableMap<String, Any>): String? {
eventPayload: MutableMap<String, Any>,
threadRelation: RelationDefaultContent?): String? {
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
"In reply to a thread",
eventBody)
val messageTextContent = MessageTextContent(
relatesTo = threadRelation,
msgType = MessageType.MSGTYPE_TEXT,
format = MessageFormat.FORMAT_MATRIX_HTML,
body = eventBody,
@ -359,6 +381,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private fun getRootThreadEventId(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
event.content.toModel<MessageRelationContent>()?.relatesTo
private fun getPreviousEventOrRoot(event: Event): String? =
event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId

View file

@ -465,7 +465,8 @@ class MessageComposerViewModel @AssistedInject constructor(
// is original event a reply?
val relationContent = state.sendMode.timelineEvent.getRelationContent()
val inReplyTo = if (state.rootThreadEventId != null) {
if (relationContent?.inReplyTo?.shouldRenderInThread() == true) {
// Thread event
if (relationContent?.shouldRenderInThread() == true) {
// Reply within a thread event
relationContent.inReplyTo?.eventId
} else {
@ -509,6 +510,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is SendMode.Reply -> {
val timelineEvent = state.sendMode.timelineEvent
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
state.rootThreadEventId?.let {
room.replyInThread(