From ecc9b59ad142d9fba234a493bfde8e8b9bcf8d76 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 8 Nov 2021 20:46:37 +0200 Subject: [PATCH] Reply In Thread, create a new thread timeline --- .../sdk/api/session/events/model/Event.kt | 61 ++++++++------ .../room/model/relation/RelationService.kt | 4 +- .../model/relation/threads/ThreadContent.kt | 28 ------- .../model/relation/threads/ThreadRelatesTo.kt | 31 ------- .../relation/threads/ThreadTextContent.kt | 28 ------- .../database/RealmSessionStoreMigration.kt | 13 ++- .../internal/database/mapper/EventMapper.kt | 5 ++ .../internal/database/model/ChunkEntity.kt | 3 + .../internal/database/model/EventEntity.kt | 10 ++- .../database/query/ChunkEntityQueries.kt | 19 +++++ .../room/relation/DefaultRelationService.kt | 14 ++-- .../room/send/LocalEchoEventFactory.kt | 11 ++- .../internal/session/room/send/TextContent.kt | 14 ++-- .../room/timeline/TokenChunkEventPersistor.kt | 54 +++++++++++- .../sync/handler/room/RoomSyncHandler.kt | 84 ++++++++++++++++++- .../home/room/detail/RoomDetailFragment.kt | 35 ++++++-- .../home/room/detail/RoomDetailViewState.kt | 5 +- .../detail/composer/TextComposerAction.kt | 2 + .../detail/composer/TextComposerViewModel.kt | 21 +++-- .../detail/composer/TextComposerViewState.kt | 3 + .../timeline/factory/MessageItemFactory.kt | 2 +- .../helper/MessageItemAttributesFactory.kt | 6 +- .../detail/timeline/item/AbsMessageItem.kt | 5 +- .../detail/RoomThreadDetailActivity.kt | 13 ++- .../detail/RoomThreadDetailFragment.kt | 10 ++- .../res/layout/item_timeline_event_base.xml | 13 +++ 26 files changed, 340 insertions(+), 154 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 169f90dbca..896d6b0e7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -96,6 +96,9 @@ data class Event( @Transient var sendStateDetails: String? = null + @Transient + var isRootThread: Boolean = false + fun sendStateError(): MatrixError? { return sendStateDetails?.let { val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) @@ -241,54 +244,54 @@ data class Event( fun Event.isTextMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_NOTICE -> true - else -> false - } + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE -> true + else -> false + } } fun Event.isImageMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_IMAGE -> true - else -> false - } + MessageType.MSGTYPE_IMAGE -> true + else -> false + } } fun Event.isVideoMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_VIDEO -> true - else -> false - } + MessageType.MSGTYPE_VIDEO -> true + else -> false + } } fun Event.isAudioMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_AUDIO -> true - else -> false - } + MessageType.MSGTYPE_AUDIO -> true + else -> false + } } fun Event.isFileMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_FILE -> true - else -> false - } + MessageType.MSGTYPE_FILE -> true + else -> false + } } fun Event.isAttachmentMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) { - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_FILE -> true - else -> false - } + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_FILE -> true + else -> false + } } fun Event.getRelationContent(): RelationDefaultContent? { @@ -299,12 +302,22 @@ fun Event.getRelationContent(): RelationDefaultContent? { } } +/** + * Returns the relation content for a specific type or null otherwise + */ +fun Event.getRelationContentForType(type: String): RelationDefaultContent? = + getRelationContent()?.takeIf { it.type == type } + fun Event.isReply(): Boolean { return getRelationContent()?.inReplyTo?.eventId != null } +fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null + +fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId + fun Event.isEdition(): Boolean { - return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null + return getRelationContentForType(RelationType.REPLACE)?.eventId != null } fun Event.getPresenceContent(): PresenceContent? { 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 20e33fec8c..226769ced4 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 @@ -131,11 +131,11 @@ interface RelationService { * Creates a thread reply for an existing timeline event * The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated * by the sdk into pills. - * @param eventToReplyInThread the event referenced by the thread reply + * @param rootThreadEventId the root thread eventId * @param replyInThreadText 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 replyInThread(eventToReplyInThread: TimelineEvent, + fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean = false): Cancelable? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt deleted file mode 100644 index 9d0fd9508a..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2021 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.api.session.room.model.relation.threads - -interface ThreadContent { - - companion object { - const val MSG_TYPE_JSON_KEY = "msgtype" - } - - val msgType: String - val body: String - val relatesTo: ThreadRelatesTo? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt deleted file mode 100644 index 4a0d1e2054..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2021 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 org.matrix.android.sdk.api.session.room.model.relation.threads - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.room.model.relation.RelationContent -import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent - -@JsonClass(generateAdapter = true) -data class ThreadRelatesTo( - @Json(name = "rel_type") override val type: String? = RelationType.THREAD, - @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 -) : RelationContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt deleted file mode 100644 index 9244b0bf7f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2021 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.api.session.room.model.relation.threads - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.room.model.message.MessageContent - -@JsonClass(generateAdapter = true) -data class ThreadTextContent( - @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, - @Json(name = "body") override val body: String, - @Json(name = "m.relates_to") override val relatesTo: ThreadRelatesTo? = null, -) : ThreadContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 05137f8105..96c32ea08f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields @@ -49,7 +50,7 @@ import timber.log.Timber internal object RealmSessionStoreMigration : RealmMigration { - const val SESSION_STORE_SCHEMA_VERSION = 18L + const val SESSION_STORE_SCHEMA_VERSION = 19L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Session from $oldVersion to $newVersion") @@ -72,6 +73,7 @@ internal object RealmSessionStoreMigration : RealmMigration { if (oldVersion <= 15) migrateTo16(realm) if (oldVersion <= 16) migrateTo17(realm) if (oldVersion <= 17) migrateTo18(realm) + if (oldVersion <= 18) migrateTo19(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -364,4 +366,13 @@ internal object RealmSessionStoreMigration : RealmMigration { realm.schema.get("RoomMemberSummaryEntity") ?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity) } + + private fun migrateTo19(realm: DynamicRealm) { + Timber.d("Step 18 -> 19") + realm.schema.get("EventEntity") + ?.addField(EventEntityFields.IS_THREAD, Boolean::class.java, FieldAttribute.INDEXED) + ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java) + realm.schema.get("ChunkEntity") + ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 613b38e340..21a93ba904 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -21,6 +21,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId +import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.model.EventEntity @@ -39,6 +41,8 @@ internal object EventMapper { eventEntity.isUseless = IsUselessResolver.isUseless(event) eventEntity.stateKey = event.stateKey eventEntity.type = event.type ?: EventType.MISSING_TYPE + eventEntity.isThread = if(event.isRootThread) true else event.isThread() + eventEntity.rootThreadEventId = if(event.isRootThread) null else event.getRootThreadEventId() eventEntity.sender = event.senderId eventEntity.originServerTs = event.originServerTs eventEntity.redacts = event.redacts @@ -93,6 +97,7 @@ internal object EventMapper { MXCryptoError.ErrorType.valueOf(errorCode) } it.mCryptoErrorReason = eventEntity.decryptionErrorReason + it.isRootThread = eventEntity.isRootThread() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 68533a3c19..2b763dd941 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class ChunkEntity(@Index var prevToken: String? = null, // Because of gaps we can have several chunks with nextToken == null @Index var nextToken: String? = null, + @Index var rootThreadEventId: String? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), var numberOfTimelineEvents: Long = 0, @@ -44,6 +45,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, val room: RealmResults? = null companion object + + fun isThreadChunk() = rootThreadEventId != null } internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index bcd30cb54b..ad889dd352 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -27,13 +27,15 @@ import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", @Index var type: String = "", + @Index var isThread: Boolean = false, + var rootThreadEventId: String? = null, var content: String? = null, var prevContent: String? = null, var isUseless: Boolean = false, @Index var stateKey: String? = null, var originServerTs: Long? = null, @Index var sender: String? = null, - // Can contain a serialized MatrixError + // Can contain a serialized MatrixError var sendStateDetails: String? = null, var age: Long? = 0, var unsignedData: String? = null, @@ -75,4 +77,10 @@ internal open class EventEntity(@Index var eventId: String = "", .findFirst() ?.canBeProcessed = true } + + /** + * Returns true if the current event is a thread root event + */ + fun isRootThread(): Boolean = isThread && rootThreadEventId == null + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt index 156a8dd767..6018305c39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -33,9 +33,11 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: val query = where(realm, roomId) if (prevToken != null) { query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken) + query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) } if (nextToken != null) { query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken) + query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) } return query.findFirst() } @@ -43,12 +45,15 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { return where(realm, roomId) .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) .findFirst() } + internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults { return realm.where() .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) + .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) .findAll() } @@ -56,6 +61,7 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() } + internal fun ChunkEntity.Companion.create( realm: Realm, prevToken: String?, @@ -66,3 +72,16 @@ internal fun ChunkEntity.Companion.create( this.nextToken = nextToken } } + +// Threads +internal fun ChunkEntity.Companion.findThreadChunkOfRoom(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? { + return where(realm, roomId) + .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .findFirst() +} + +internal fun ChunkEntity.Companion.findAllThreadChunkOfRoom(realm: Realm, roomId: String): RealmResults { + return where(realm, roomId) + .isNotNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) + .findAll() +} 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 7dec4ab3de..833f056ceb 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 @@ -159,11 +159,15 @@ internal class DefaultRelationService @AssistedInject constructor( } } - override fun replyInThread(eventToReplyInThread: TimelineEvent, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable? { - val event = eventFactory.createThreadTextEvent(eventToReplyInThread, TextContent(replyInThreadText.toString())) - .also { - saveLocalEcho(it) - } + override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable { + val event = eventFactory.createThreadTextEvent( + rootThreadEventId = rootThreadEventId, + roomId = roomId, + text = replyInThreadText.toString(), + autoMarkdown = autoMarkdown) +// .also { +// saveLocalEcho(it) +// } return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } 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 2e1a95feb5..b69e868338 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 @@ -51,8 +51,6 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent -import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadTextContent -import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadRelatesTo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.isReply @@ -60,7 +58,6 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils -import timber.log.Timber import javax.inject.Inject /** @@ -346,11 +343,13 @@ internal class LocalEchoEventFactory @Inject constructor( /** * Creates a thread event related to the already existing event */ - fun createThreadTextEvent(eventToReplyInThread: TimelineEvent, textContent: TextContent): Event = + fun createThreadTextEvent(rootThreadEventId: String, roomId:String, text: String, autoMarkdown: Boolean): Event = createEvent( - eventToReplyInThread.roomId, + roomId, EventType.MESSAGE, - textContent.toThreadTextContent(eventToReplyInThread).toContent()) + createTextContent(text, autoMarkdown) + .toThreadTextContent(rootThreadEventId) + .toContent()) private fun dummyOriginServerTs(): Long { return System.currentTimeMillis() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt index c3f4f72834..d3e0189f4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt @@ -16,13 +16,11 @@ package org.matrix.android.sdk.internal.session.room.send -import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.message.MessageFormat 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.threads.ThreadTextContent -import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadRelatesTo -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply @@ -45,11 +43,13 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT) ) } -fun TextContent.toThreadTextContent(eventToReplyInThread: TimelineEvent, msgType: String = MessageType.MSGTYPE_TEXT): ThreadTextContent { - return ThreadTextContent( +fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { + return MessageTextContent( msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, body = text, - relatesTo = ThreadRelatesTo(eventId = eventToReplyInThread.eventId) + relatesTo = RelationDefaultContent(RelationType.THREAD, rootThreadEventId), + formattedBody = formattedText ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index dbcc37a918..7b873166b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,7 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import io.realm.kotlin.createObject import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState @@ -28,6 +30,8 @@ import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.merge import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -37,6 +41,7 @@ import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase @@ -221,7 +226,15 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + Timber.i("------> [TokenChunkEventPersistor] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}") + + addTimelineEventToChunk( + realm = realm, + roomId = roomId, + eventEntity = eventEntity, + currentChunk = currentChunk, + direction = direction, + roomMemberContentsByUser = roomMemberContentsByUser) } // Find all the chunks which contain at least one event from the list of eventIds val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) @@ -247,4 +260,43 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } } + + /** + * Adds a timeline event to the correct chunk. If there is a thread detected will be added + * to a specific chunk + */ + private fun addTimelineEventToChunk(realm: Realm, + roomId: String, + eventEntity: EventEntity, + currentChunk: ChunkEntity, + direction: PaginationDirection, + roomMemberContentsByUser: Map) { + val rootThreadEventId = eventEntity.rootThreadEventId + if (eventEntity.isThread && rootThreadEventId != null) { + val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId) + threadChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + markEventAsRootEvent(realm, rootThreadEventId) + if (threadChunk.isValid) + RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(threadChunk) + } else { + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + } + } + + private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) { + val rootThreadEvent = EventEntity + .where(realm, rootThreadEventId) + .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return + rootThreadEvent.isThread = true + } + + /** + * Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity + */ + private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity { + return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId) + ?: realm.createObject().apply { + this.rootThreadEventId = rootThreadEventId + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8c4af81c99..ea5aedeee9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -21,31 +21,41 @@ import io.realm.kotlin.createObject import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.RoomSync import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addTimelineEvent +import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findAllThreadChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom +import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -58,8 +68,11 @@ import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy @@ -356,6 +369,21 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val eventIds = ArrayList(eventList.size) val roomMemberContentsByUser = HashMap() +///////////////////// + // There is only one chunk per room + + val threadChunks = ChunkEntity.findAllThreadChunkOfRoom(realm, roomId) + + val tc = threadChunks.joinToString { chunk -> + var output = "\n----------------\n------> [${chunk.timelineEvents.size}] rootThreadEventId = ${chunk.rootThreadEventId}" + "\n" + output += chunk.timelineEvents + .joinToString("") { + "------> " + "eventId:[${it?.eventId}] payload:[${getValueFromPayload(it.root?.let { root -> EventMapper.map(root).mxDecryptionResult }?.payload, "body")}]\n" + } + output + } + Timber.i("------> Chunks (${threadChunks.size})$tc") +///////////////////// for (event in eventList) { if (event.eventId == null || event.senderId == null || event.type == null) { continue @@ -385,7 +413,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle rootStateEvent?.asDomain()?.getFixedRoomMemberContent() } - chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + Timber.i("------> [RoomSyncHandler] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}") + + addTimelineEventToChunk( + realm = realm, + roomId = roomId, + eventEntity = eventEntity, + chunkEntity = chunkEntity, + roomEntity = roomEntity, + roomMemberContentsByUser = roomMemberContentsByUser) + // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) @@ -412,9 +449,54 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } // posting new events to timeline if any is registered timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds) + return chunkEntity } + /** + * Adds a timeline event to the correct chunk. If there is a thread detected will be added + * to a specific chunk + */ + private fun addTimelineEventToChunk(realm: Realm, + roomId: String, + eventEntity: EventEntity, + chunkEntity: ChunkEntity, + roomEntity: RoomEntity, + roomMemberContentsByUser: Map) { + val rootThreadEventId = eventEntity.rootThreadEventId + if (eventEntity.isThread && rootThreadEventId != null) { + val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId) + threadChunk.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + markEventAsRootEvent(realm, rootThreadEventId) + roomEntity.addIfNecessary(threadChunk) + } else { + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + } + } + + @Suppress("UNCHECKED_CAST") + private fun getValueFromPayload(payload: JsonDict?, key: String): String? { + val content = payload?.get("content") as? JsonDict + return content?.get(key) as? String + } + + /** + * Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity + */ + private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity { + return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId) + ?: realm.createObject().apply { + this.rootThreadEventId = rootThreadEventId + } + } + + private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String){ + val rootThreadEvent = EventEntity + .where(realm, rootThreadEventId) + .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return + rootThreadEvent.isThread = true + } + private fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9157ec3a2c..bbccc78c9c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -49,6 +49,7 @@ import androidx.core.text.toSpannable import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener @@ -228,7 +229,8 @@ data class RoomDetailArgs( val roomId: String, val eventId: String? = null, val sharedData: SharedData? = null, - val openShareSpaceForId: String? = null + val openShareSpaceForId: String? = null, + val roomThreadDetailArgs: RoomThreadDetailArgs? = null ) : Parcelable class RoomDetailFragment @Inject constructor( @@ -352,7 +354,12 @@ class RoomDetailFragment @Inject constructor( ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) - setupToolbar(views.roomToolbar) + if (isThreadTimeLine()) { + views.roomToolbar.isGone = true + } else { + setupToolbar(views.roomToolbar) + } + setupThreadIfNeeded() setupRecyclerView() setupComposer() setupNotificationView() @@ -390,10 +397,10 @@ class RoomDetailFragment @Inject constructor( return@onEach } when (mode) { - is SendMode.REGULAR -> renderRegularMode(mode.text) - is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) - is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) - is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) + is SendMode.REGULAR -> renderRegularMode(mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) } } @@ -902,6 +909,7 @@ class RoomDetailFragment @Inject constructor( override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) + if (isThreadTimeLine()) return // We use a custom layout for this menu item, so we need to set a ClickListener menu.findItem(R.id.open_matrix_apps)?.let { menuItem -> menuItem.actionView.setOnClickListener { @@ -915,6 +923,12 @@ class RoomDetailFragment @Inject constructor( } override fun onPrepareOptionsMenu(menu: Menu) { + if (isThreadTimeLine()) { + menu.forEach { + it.isVisible = false + } + return + } menu.forEach { it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) } @@ -1180,6 +1194,12 @@ class RoomDetailFragment @Inject constructor( // PRIVATE METHODS ***************************************************************************** + private fun setupThreadIfNeeded(){ + getRootThreadEventId()?.let{ + textComposerViewModel.handle(TextComposerAction.EnterReplyInThreadTimeline(it)) + } + } + private fun setupRecyclerView() { timelineEventController.callback = this timelineEventController.timeline = roomDetailViewModel.timeline @@ -2203,4 +2223,7 @@ class RoomDetailFragment @Inject constructor( } } } + + fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null + fun getRootThreadEventId(): String? = roomDetailArgs.roomThreadDetailArgs?.eventId } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 042a415b47..3266ae60e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -65,8 +65,9 @@ data class RoomDetailViewState( val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, val hasFailedSending: Boolean = false, - val jitsiState: JitsiState = JitsiState() -) : MavericksState { + val jitsiState: JitsiState = JitsiState(), + val rootThreadEventId: String? = null + ) : MavericksState { constructor(args: RoomDetailArgs) : this( roomId = args.roomId, 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 7725400187..48f6c84983 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,4 +28,6 @@ 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 742d2848a1..1541d5738b 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 @@ -42,6 +42,7 @@ import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -88,6 +89,8 @@ class TextComposerViewModel @AssistedInject constructor( is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) + is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action) + } } @@ -95,6 +98,10 @@ class TextComposerViewModel @AssistedInject constructor( copy(isVoiceRecording = action.isRecording) } + private fun handleEnterReplyInThreadTimeline(action: TextComposerAction.EnterReplyInThreadTimeline) = setState { + copy(rootThreadEventId = action.rootThreadEventId) + } + private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { setState { // Makes sure currentComposerText is upToDate when accessing further setState @@ -151,11 +158,15 @@ class TextComposerViewModel @AssistedInject constructor( private fun handleSendMessage(action: TextComposerAction.SendMessage) { withState { state -> when (state.sendMode) { - is SendMode.REGULAR -> { + is SendMode.REGULAR -> { when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room - room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + if (state.rootThreadEventId != null) + room.replyInThread(state.rootThreadEventId, action.text.toString(), action.autoMarkdown) + else + room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) + _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } @@ -386,7 +397,7 @@ class TextComposerViewModel @AssistedInject constructor( } }.exhaustive } - is SendMode.EDIT -> { + is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId if (inReplyTo != null) { @@ -409,7 +420,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val textMsg = messageContent?.body @@ -430,7 +441,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _viewEvents.post(TextComposerViewEvents.MessageSent) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index 3110aa8dc3..36bdc4f5b2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -46,6 +46,7 @@ data class TextComposerViewState( val canSendMessage: Boolean = true, val isVoiceRecording: Boolean = false, val isSendButtonVisible: Boolean = false, + val rootThreadEventId: String? = null, val sendMode: SendMode = SendMode.REGULAR("", false) ) : MavericksState { @@ -53,4 +54,6 @@ data class TextComposerViewState( get() = canSendMessage && !isVoiceRecording constructor(args: RoomDetailArgs) : this(roomId = args.roomId) + + fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 98deaaf9c3..54cdb6db09 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -149,7 +149,7 @@ class MessageItemFactory @Inject constructor( // This is an edit event, we should display it when debugging as a notice event return noticeItemFactory.create(params) } - val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) + val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, event.root.isRootThread) // val all = event.root.toContent() // val ev = all.toModel() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 679613d262..80b36fa69f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -31,7 +31,8 @@ class MessageItemAttributesFactory @Inject constructor( fun create(messageContent: Any?, informationData: MessageInformationData, - callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { + callback: TimelineEventController.Callback?, + isRootThread: Boolean = false): AbsMessageItem.Attributes { return AbsMessageItem.Attributes( avatarSize = avatarSizeProvider.avatarSize, informationData = informationData, @@ -49,7 +50,8 @@ class MessageItemAttributesFactory @Inject constructor( reactionPillCallback = callback, avatarCallback = callback, readReceiptsCallback = callback, - emojiTypeFace = emojiCompatFontProvider.typeface + emojiTypeFace = emojiCompatFontProvider.typeface, + isRootThread = isRootThread ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index b53495fdaf..f6672a1d7c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -98,6 +98,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem // Render send state indicator holder.sendStateImageView.render(attributes.informationData.sendStateDecoration) holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA + holder.isThread.isVisible = attributes.isRootThread } override fun unbind(holder: H) { @@ -117,6 +118,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem val timeView by bind(R.id.messageTimeView) val sendStateImageView by bind(R.id.messageSendStateImageView) val eventSendingIndicator by bind(R.id.eventSendingIndicator) + val isThread by bind(R.id.messageIsThread) } /** @@ -133,7 +135,8 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, - val emojiTypeFace: Typeface? = null + val emojiTypeFace: Typeface? = null, + val isRootThread: Boolean = false ) : AbsBaseMessageItem.Attributes { // Have to override as it's used to diff epoxy items diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt index b8a58e178d..c82fa353e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt @@ -25,6 +25,8 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomThreadDetailBinding import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.RoomDetailFragment import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -65,7 +67,16 @@ class RoomThreadDetailActivity : VectorBaseActivity $eventId isThread: ${EventMapper.map(r).isThread()}") +// } +// } +//// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}" } private fun initTextComposer(){ diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index cb6f701bb4..f9d4314813 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -200,4 +200,17 @@ + + \ No newline at end of file