From ab87937e5bff9edceed55bcfb225ad442dec5d05 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 20 Oct 2021 18:39:59 +0300 Subject: [PATCH 001/130] Threads init commit --- .../api/session/events/model/RelationType.kt | 3 + .../room/model/relation/RelationService.kt | 15 ++++ .../model/relation/threads/ThreadContent.kt | 28 ++++++ .../model/relation/threads/ThreadRelatesTo.kt | 31 +++++++ .../relation/threads/ThreadTextContent.kt | 28 ++++++ .../room/relation/DefaultRelationService.kt | 6 ++ .../room/send/LocalEchoEventFactory.kt | 12 +++ .../internal/session/room/send/TextContent.kt | 12 +++ vector/src/main/AndroidManifest.xml | 3 + .../im/vector/app/core/di/FragmentModule.kt | 6 ++ .../im/vector/app/core/di/ScreenComponent.kt | 4 + .../home/room/detail/RoomDetailFragment.kt | 12 +++ .../timeline/action/EventSharedAction.kt | 4 + .../action/MessageActionsViewModel.kt | 21 +++++ .../home/room/threads/RoomThreadsActivity.kt | 66 ++++++++++++++ .../detail/RoomThreadDetailActivity.kt | 64 ++++++++++++++ .../detail/RoomThreadDetailFragment.kt | 57 ++++++++++++ .../main/res/drawable/ic_reply_in_thread.xml | 24 +++++ .../layout/activity_room_thread_detail.xml | 88 +++++++++++++++++++ .../main/res/layout/activity_room_threads.xml | 88 +++++++++++++++++++ .../main/res/layout/fragment_room_detail.xml | 2 +- .../layout/fragment_room_thread_detail.xml | 33 +++++++ .../src/main/res/menu/menu_room_threads.xml | 9 ++ vector/src/main/res/values/strings.xml | 4 + 24 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt create mode 100644 vector/src/main/res/drawable/ic_reply_in_thread.xml create mode 100644 vector/src/main/res/layout/activity_room_thread_detail.xml create mode 100644 vector/src/main/res/layout/activity_room_threads.xml create mode 100644 vector/src/main/res/layout/fragment_room_thread_detail.xml create mode 100644 vector/src/main/res/menu/menu_room_threads.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt index 7d827f871b..653798c29c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -28,6 +28,9 @@ object RelationType { /** Lets you define an event which references an existing event.*/ const val REFERENCE = "m.reference" + /** Lets you define an event which is a reply to an existing event.*/ + const val THREAD = "m.thread" + /** Lets you define an event which adds a response to an existing event.*/ const val RESPONSE = "org.matrix.response" } 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 59d84ef40f..20e33fec8c 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 @@ -44,6 +44,9 @@ import org.matrix.android.sdk.api.util.Optional * m.reference - lets you define an event which references an existing event. * When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads). * These are primarily intended for handling replies (and in future threads). + * + * m.thread - lets you define an event which is a thread reply to an existing event. + * When aggregated, returns the most thread event */ interface RelationService { @@ -123,4 +126,16 @@ interface RelationService { * @return the LiveData of EventAnnotationsSummary */ fun getEventAnnotationsSummaryLive(eventId: String): LiveData> + + /** + * 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 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, + 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 new file mode 100644 index 0000000000..9d0fd9508a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadContent.kt @@ -0,0 +1,28 @@ +/* + * 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 new file mode 100644 index 0000000000..4a0d1e2054 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadRelatesTo.kt @@ -0,0 +1,31 @@ +/* + * 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 new file mode 100644 index 0000000000..9244b0bf7f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/threads/ThreadTextContent.kt @@ -0,0 +1,28 @@ +/* + * 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/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 07927b1412..b3afc6ad46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.TextContent import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -158,6 +159,11 @@ internal class DefaultRelationService @AssistedInject constructor( } } + override fun replyInThread(eventToReplyInThread: TimelineEvent, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable? { + val event = eventFactory.createThreadTextEvent(eventToReplyInThread, TextContent(replyInThreadText.toString())) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + } + /** * Saves the event in database as a local echo. * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. 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 8dd0c59387..2e1a95feb5 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,6 +51,8 @@ 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 @@ -58,6 +60,7 @@ 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 /** @@ -340,6 +343,15 @@ internal class LocalEchoEventFactory @Inject constructor( ) } + /** + * Creates a thread event related to the already existing event + */ + fun createThreadTextEvent(eventToReplyInThread: TimelineEvent, textContent: TextContent): Event = + createEvent( + eventToReplyInThread.roomId, + EventType.MESSAGE, + textContent.toThreadTextContent(eventToReplyInThread).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 efc0b55abf..c3f4f72834 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,9 +16,13 @@ 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.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.util.ContentUtils.extractUsefulTextFromHtmlReply import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply @@ -41,6 +45,14 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT) ) } +fun TextContent.toThreadTextContent(eventToReplyInThread: TimelineEvent, msgType: String = MessageType.MSGTYPE_TEXT): ThreadTextContent { + return ThreadTextContent( + msgType = msgType, + body = text, + relatesTo = ThreadRelatesTo(eventId = eventToReplyInThread.eventId) + ) +} + fun TextContent.removeInReplyFallbacks(): TextContent { return copy( text = extractUsefulTextFromReply(this.text), diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 376e0e869a..2b7b445ad5 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -179,6 +179,9 @@ + + + diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 3bc8e30851..4c80f4aa35 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -58,6 +58,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.detail.RoomDetailFragment import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.list.RoomListFragment +import im.vector.app.features.home.room.threads.detail.RoomThreadDetailFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment @@ -834,4 +835,9 @@ interface FragmentModule { @IntoMap @FragmentKey(SpaceLeaveAdvancedFragment::class) fun bindSpaceLeaveAdvancedFragment(fragment: SpaceLeaveAdvancedFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomThreadDetailFragment::class) + fun bindRoomThreadDetailFragment(fragment: RoomThreadDetailFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 76b511d2bd..9a095a3c79 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -52,6 +52,8 @@ import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.home.room.list.RoomListModule import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import im.vector.app.features.home.room.threads.RoomThreadsActivity +import im.vector.app.features.home.room.threads.detail.RoomThreadDetailActivity import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.invite.VectorInviteView @@ -174,6 +176,8 @@ interface ScreenComponent { fun inject(activity: SpaceManageActivity) fun inject(activity: RoomJoinRuleActivity) fun inject(activity: SpaceLeaveAdvancedActivity) + fun inject(activity: RoomThreadsActivity) + fun inject(activity: RoomThreadDetailActivity) /* ========================================================================================== * BottomSheets 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 e9948e6cf4..8380774a49 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 @@ -161,6 +161,8 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet +import im.vector.app.features.home.room.threads.detail.RoomThreadDetailArgs +import im.vector.app.features.home.room.threads.detail.RoomThreadDetailActivity import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor @@ -1957,6 +1959,16 @@ class RoomDetailFragment @Inject constructor( requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } + is EventSharedAction.ReplyInThread -> { + if (!views.voiceMessageRecorderView.isActive()) { + context?.let { + val roomThreadDetailArgs = RoomThreadDetailArgs(roomDetailArgs.roomId,action.eventId) + startActivity(RoomThreadDetailActivity.newIntent(it, roomThreadDetailArgs)) + } + } else { + requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + } + } is EventSharedAction.CopyPermalink -> { val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index d9ee7f3ccf..c57d844974 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -48,6 +48,10 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Reply(val eventId: String) : EventSharedAction(R.string.reply, R.drawable.ic_reply) + data class ReplyInThread(val eventId: String) : + // TODO add translations + EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) + data class Share(val eventId: String, val messageContent: MessageContent) : EventSharedAction(R.string.share, R.drawable.ic_share) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index b4fff6eb3d..bdd5177058 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -326,6 +326,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Reply(eventId)) } + // *** Testing Threads **** + if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.ReplyInThread(eventId)) + } + if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { add(EventSharedAction.Edit(eventId)) } @@ -412,6 +417,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } + private fun canReplyInThread(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { + // Only event of type EventType.MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + if (!actionPermissions.canSendMessage) return false + return when (messageContent?.msgType) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> true + else -> false + } + } + private fun canQuote(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { // Only event of type EventType.MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt new file mode 100644 index 0000000000..0ad1d02ffb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt @@ -0,0 +1,66 @@ +/* + * Copyright 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 im.vector.app.features.home.room.threads + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.widget.SearchView +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityFilteredRoomsBinding +import im.vector.app.databinding.ActivityRoomThreadsBinding +import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.home.room.list.RoomListFragment +import im.vector.app.features.home.room.list.RoomListParams + +class RoomThreadsActivity : VectorBaseActivity() { + +// private val roomListFragment: RoomListFragment? +// get() { +// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment +// } + + override fun getBinding() = ActivityRoomThreadsBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getMenuRes() = R.menu.menu_room_threads + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + configureToolbar(views.roomThreadsToolbar) +// if (isFirstCreation()) { +// val params = RoomListParams(RoomListDisplayMode.FILTERED) +// replaceFragment(R.id.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) +// } + } + + companion object { + private const val FRAGMENT_TAG = "RoomListFragment" + + fun newIntent(context: Context): Intent { + return Intent(context, RoomThreadsActivity::class.java) + } + } +} 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 new file mode 100644 index 0000000000..96b65d0272 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt @@ -0,0 +1,64 @@ +/* + * Copyright 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 im.vector.app.features.home.room.threads.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityRoomThreadDetailBinding + +class RoomThreadDetailActivity : VectorBaseActivity() { + +// private val roomThreadDetailFragment: RoomThreadDetailFragment? +// get() { +// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment +// } + + override fun getBinding() = ActivityRoomThreadDetailBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getMenuRes() = R.menu.menu_room_threads + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + configureToolbar(views.roomThreadDetailToolbar) + if (isFirstCreation()) { + val roomThreadDetailArgs: RoomThreadDetailArgs? = intent?.extras?.getParcelable(EXTRA_ROOM_THREAD_DETAIL_ARGS) + replaceFragment(R.id.roomThreadDetailFragmentContainer, RoomThreadDetailFragment::class.java, roomThreadDetailArgs, FRAGMENT_TAG) + } + } + + companion object { + private const val FRAGMENT_TAG = "RoomThreadDetailFragment" + const val EXTRA_ROOM_THREAD_DETAIL_ARGS = "EXTRA_ROOM_THREAD_DETAIL_ARGS" + + fun newIntent(context: Context, roomThreadDetailArgs: RoomThreadDetailArgs): Intent { + return Intent(context, RoomThreadDetailActivity::class.java).apply { + putExtra(EXTRA_ROOM_THREAD_DETAIL_ARGS, roomThreadDetailArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt new file mode 100644 index 0000000000..c50fdcd9ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt @@ -0,0 +1,57 @@ +/* + * Copyright 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 im.vector.app.features.home.room.threads.detail + +import android.annotation.SuppressLint +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomThreadDetailBinding +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +@Parcelize +data class RoomThreadDetailArgs( + val roomId: String, + val eventId: String? = null, +) : Parcelable + +class RoomThreadDetailFragment @Inject constructor( + private val session: Session +) : + VectorBaseFragment() { + private val roomThreadDetailArgs: RoomThreadDetailArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomThreadDetailBinding { + return FragmentRoomThreadDetailBinding.inflate(inflater, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + @SuppressLint("SetTextI18n") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}" + } +} diff --git a/vector/src/main/res/drawable/ic_reply_in_thread.xml b/vector/src/main/res/drawable/ic_reply_in_thread.xml new file mode 100644 index 0000000000..955dc27f45 --- /dev/null +++ b/vector/src/main/res/drawable/ic_reply_in_thread.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_room_thread_detail.xml b/vector/src/main/res/layout/activity_room_thread_detail.xml new file mode 100644 index 0000000000..94c52ab959 --- /dev/null +++ b/vector/src/main/res/layout/activity_room_thread_detail.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_room_threads.xml b/vector/src/main/res/layout/activity_room_threads.xml new file mode 100644 index 0000000000..b469c7de42 --- /dev/null +++ b/vector/src/main/res/layout/activity_room_threads.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index c0ac3170e5..7725cd5e92 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -18,7 +18,7 @@ android:layout_height="wrap_content" android:minHeight="48dp" android:visibility="gone" - tools:visibility="visible" /> + tools:visibility="gone" /> + + + + + + + diff --git a/vector/src/main/res/menu/menu_room_threads.xml b/vector/src/main/res/menu/menu_room_threads.xml new file mode 100644 index 0000000000..3d4478332a --- /dev/null +++ b/vector/src/main/res/menu/menu_room_threads.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 7fa4918266..591cc152b9 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1021,6 +1021,9 @@ INVITED JOINED + + Filter Threads in room + Reason for reporting this content Do you want to hide all messages from this user?\n\nNote that this action will restart the app and it may take some time. @@ -2171,6 +2174,7 @@ Edit Reply + Reply In Thread Retry "Join a room to start using the app." From cb6376670b785dd8b288717c5d615ffcec2934ca Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 21 Oct 2021 12:25:43 +0300 Subject: [PATCH 002/130] Add room avatar to threads activities --- .../home/room/detail/RoomDetailFragment.kt | 8 +++-- .../home/room/detail/RoomDetailViewModel.kt | 2 ++ .../detail/RoomThreadDetailActivity.kt | 33 +++++++++++++++---- .../detail/RoomThreadDetailFragment.kt | 13 ++------ .../detail/arguments/RoomThreadDetailArgs.kt | 28 ++++++++++++++++ 5 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt 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 8380774a49..9157ec3a2c 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 @@ -161,7 +161,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet -import im.vector.app.features.home.room.threads.detail.RoomThreadDetailArgs +import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs import im.vector.app.features.home.room.threads.detail.RoomThreadDetailActivity import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -1962,7 +1962,11 @@ class RoomDetailFragment @Inject constructor( is EventSharedAction.ReplyInThread -> { if (!views.voiceMessageRecorderView.isActive()) { context?.let { - val roomThreadDetailArgs = RoomThreadDetailArgs(roomDetailArgs.roomId,action.eventId) + val roomThreadDetailArgs = RoomThreadDetailArgs( + roomId = roomDetailArgs.roomId, + displayName = roomDetailViewModel.getRoomSummary()?.displayName, + avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl, + eventId = action.eventId) startActivity(RoomThreadDetailActivity.newIntent(it, roomThreadDetailArgs)) } } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 0c0e5ee6cd..b9622ae9ae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -282,6 +282,8 @@ class RoomDetailViewModel @AssistedInject constructor( fun getOtherUserIds() = room.roomSummary()?.otherMemberIds + fun getRoomSummary() = room.roomSummary() + override fun handle(action: RoomDetailAction) { when (action) { is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) 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 96b65d0272..25dd1e6031 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 @@ -24,9 +24,16 @@ import im.vector.app.core.di.ScreenComponent 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.threads.detail.arguments.RoomThreadDetailArgs +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject class RoomThreadDetailActivity : VectorBaseActivity() { + @Inject + lateinit var avatarRenderer: AvatarRenderer + // private val roomThreadDetailFragment: RoomThreadDetailFragment? // get() { // return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment @@ -44,20 +51,34 @@ class RoomThreadDetailActivity : VectorBaseActivity() { +) : VectorBaseFragment() { + private val roomThreadDetailArgs: RoomThreadDetailArgs by args() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomThreadDetailBinding { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt new file mode 100644 index 0000000000..5fce24cd0d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt @@ -0,0 +1,28 @@ +/* + * 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 im.vector.app.features.home.room.threads.detail.arguments + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RoomThreadDetailArgs( + val roomId: String, + val displayName: String?, + val avatarUrl: String?, + val eventId: String? = null, +) : Parcelable From a2a2315f9c00e63054224c3788d0bbf6b7b70214 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 21 Oct 2021 16:53:20 +0300 Subject: [PATCH 003/130] Make room thread detail text composer visible --- .../home/room/threads/detail/RoomThreadDetailActivity.kt | 1 - .../home/room/threads/detail/RoomThreadDetailFragment.kt | 7 +++++++ vector/src/main/res/layout/fragment_room_thread_detail.xml | 7 +++---- 3 files changed, 10 insertions(+), 5 deletions(-) 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 25dd1e6031..b8a58e178d 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 @@ -51,7 +51,6 @@ class RoomThreadDetailActivity : VectorBaseActivity Date: Thu, 4 Nov 2021 09:33:32 +0200 Subject: [PATCH 004/130] Add changelog file --- .../sdk/api/session/events/model/RelationType.kt | 3 ++- .../session/room/relation/DefaultRelationService.kt | 5 ++++- .../room/threads/detail/RoomThreadDetailFragment.kt | 2 +- .../main/res/layout/fragment_room_thread_detail.xml | 11 +++++------ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt index 653798c29c..6546258766 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -29,7 +29,8 @@ object RelationType { const val REFERENCE = "m.reference" /** Lets you define an event which is a reply to an existing event.*/ - const val THREAD = "m.thread" +// const val THREAD = "m.thread" + const val THREAD = "io.element.thread" /** Lets you define an event which adds a response to an existing event.*/ const val RESPONSE = "org.matrix.response" 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 b3afc6ad46..7dec4ab3de 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 @@ -56,7 +56,7 @@ internal class DefaultRelationService @AssistedInject constructor( private val timelineEventMapper: TimelineEventMapper, @SessionDatabase private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) : - RelationService { + RelationService { @AssistedFactory interface Factory { @@ -161,6 +161,9 @@ 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) + } return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt index 3332d7caa0..532c67aaf8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt @@ -47,7 +47,7 @@ class RoomThreadDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initTextComposer() - views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}" +// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}" } private fun initTextComposer(){ diff --git a/vector/src/main/res/layout/fragment_room_thread_detail.xml b/vector/src/main/res/layout/fragment_room_thread_detail.xml index 48e4896730..cadc819d28 100644 --- a/vector/src/main/res/layout/fragment_room_thread_detail.xml +++ b/vector/src/main/res/layout/fragment_room_thread_detail.xml @@ -6,12 +6,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - Date: Mon, 8 Nov 2021 20:46:37 +0200 Subject: [PATCH 005/130] 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 From 8c539426e63628317ab27a27799a7bcfb24ed9bc Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 15 Nov 2021 19:17:13 +0200 Subject: [PATCH 006/130] - Thread Summary along with optimization - Create new thread & reply to thread --- build.gradle | 5 + matrix-sdk-android/build.gradle | 2 + .../sdk/api/session/events/model/Event.kt | 18 ++- .../sdk/api/session/room/timeline/Timeline.kt | 2 +- .../sdk/api/session/threads/ThreadDetails.kt | 26 ++++ .../database/RealmSessionStoreMigration.kt | 10 +- .../database/helper/ThreadEventsHelper.kt | 88 ++++++++++++++ .../internal/database/mapper/EventMapper.kt | 23 +++- .../internal/database/model/ChunkEntity.kt | 2 - .../internal/database/model/EventEntity.kt | 17 +-- .../database/query/ChunkEntityQueries.kt | 19 +-- .../database/query/EventEntityQueries.kt | 5 + .../query/TimelineEventEntityQueries.kt | 4 + .../session/room/timeline/DefaultTimeline.kt | 22 +++- .../session/room/timeline/PaginationTask.kt | 1 + .../room/timeline/TokenChunkEventPersistor.kt | 112 +++++++++++------- .../sync/handler/room/RoomSyncHandler.kt | 96 +++------------ vector/build.gradle | 3 + .../home/room/detail/RoomDetailFragment.kt | 9 +- .../home/room/detail/RoomDetailViewModel.kt | 3 +- .../home/room/detail/RoomDetailViewState.kt | 3 +- .../detail/composer/TextComposerViewModel.kt | 6 - .../detail/composer/TextComposerViewState.kt | 4 +- .../timeline/TimelineEventController.kt | 15 ++- .../timeline/action/MessageActionState.kt | 9 +- .../action/MessageActionsBottomSheet.kt | 5 +- .../action/MessageActionsViewModel.kt | 22 ++-- .../action/TimelineEventFragmentArgs.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 2 +- .../timeline/factory/TimelineItemFactory.kt | 10 +- .../factory/TimelineItemFactoryParams.kt | 3 + .../helper/MessageItemAttributesFactory.kt | 5 +- .../helper/TimelineEventVisibilityHelper.kt | 23 ++-- .../detail/timeline/item/AbsMessageItem.kt | 29 ++++- .../detail/timeline/merged/MergedTimelines.kt | 2 +- .../main/res/drawable/ic_reply_in_thread.xml | 32 ++--- .../main/res/drawable/ic_thread_summary.xml | 11 ++ .../res/layout/item_timeline_event_base.xml | 18 +-- .../res/layout/view_thread_room_summary.xml | 74 ++++++++++++ 40 files changed, 481 insertions(+), 264 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt create mode 100644 vector/src/main/res/drawable/ic_thread_summary.xml create mode 100644 vector/src/main/res/layout/view_thread_room_summary.xml diff --git a/build.gradle b/build.gradle index 93f3e17f34..7bde8422b3 100644 --- a/build.gradle +++ b/build.gradle @@ -147,6 +147,11 @@ project(":diff-match-patch") { } } +// Global configurations across all modules +ext { + isThreadingEnabled = true +} + //project(":matrix-sdk-android") { // sonarqube { // properties { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 43ca243ec5..176edb97f6 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -38,6 +38,8 @@ android { resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" + // Indicates whether or not threading support is enabled + buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}" defaultConfig { consumerProguardFiles 'proguard-rules.pro' } 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 896d6b0e7b..ccf98f7754 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 @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.threads.ThreadDetails import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent @@ -97,7 +98,7 @@ data class Event( var sendStateDetails: String? = null @Transient - var isRootThread: Boolean = false + var threadDetails: ThreadDetails? = null fun sendStateError(): MatrixError? { return sendStateDetails?.let { @@ -124,6 +125,7 @@ data class Event( it.mCryptoErrorReason = mCryptoErrorReason it.sendState = sendState it.ageLocalTs = ageLocalTs + it.threadDetails = threadDetails } } @@ -186,6 +188,16 @@ data class Event( return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } } + fun getDecryptedMessageText(): String { + return getValueFromPayload(mxDecryptionResult?.payload).orEmpty() + } + + @Suppress("UNCHECKED_CAST") + private fun getValueFromPayload(payload: JsonDict?, key: String = "body"): String? { + val content = payload?.get("content") as? JsonDict + return content?.get(key) as? String + } + /** * Tells if the event is redacted */ @@ -218,7 +230,7 @@ data class Event( if (mCryptoError != other.mCryptoError) return false if (mCryptoErrorReason != other.mCryptoErrorReason) return false if (sendState != other.sendState) return false - + if (threadDetails != other.threadDetails) return false return true } @@ -237,6 +249,8 @@ data class Event( result = 31 * result + (mCryptoError?.hashCode() ?: 0) result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) result = 31 * result + sendState.hashCode() + result = 31 * result + threadDetails.hashCode() + return result } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 06c88db831..4c07250e4c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -43,7 +43,7 @@ interface Timeline { /** * This must be called before any other method after creating the timeline. It ensures the underlying database is open */ - fun start() + fun start(rootThreadEventId: String? = null) /** * This must be called when you don't need the timeline. It ensures the underlying database get closed. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt new file mode 100644 index 0000000000..04dbb18797 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -0,0 +1,26 @@ +/* + * 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.threads + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +data class ThreadDetails( + val isRootThread: Boolean = false, + val numberOfThreads: Int = 0, + val threadSummarySenderInfo: SenderInfo? = null, + val threadSummaryLatestTextMessage: String? = null +) 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 96c32ea08f..111fc50e56 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 @@ -369,10 +369,12 @@ internal object RealmSessionStoreMigration : RealmMigration { private fun migrateTo19(realm: DynamicRealm) { Timber.d("Step 18 -> 19") + val eventEntity = realm.schema.get("TimelineEventEntity") ?: return + 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) + ?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED) + ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) + ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java) + ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt new file mode 100644 index 0000000000..597e08e307 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -0,0 +1,88 @@ +/* + * 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.internal.database.helper + +import io.realm.Realm +import io.realm.RealmResults +import io.realm.Sort +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereRoomId + +/** + * Finds the root thread event and update it with the latest message summary along with the number + * of threads included. If there is no root thread event no action is done + */ +internal fun Map.updateThreadSummaryIfNeeded() { + + if (!BuildConfig.THREADING_ENABLED) return + + for ((rootThreadEventId, eventEntity) in this) { + + eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let { + + if (it.isNullOrEmpty()) return@let + + val latestMessage = it.firstOrNull() + + // If this is a thread message, find its root event if exists + val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity + + rootThreadEvent?.markEventAsRoot( + threadsCounted = it.size, + latestMessageTimelineEventEntity = latestMessage + ) + } + } +} + +/** + * Finds the root event of the the current thread event message. + * Returns the EventEntity or null if the root event do not exist + */ +internal fun EventEntity.findRootThreadEvent(): EventEntity? = + rootThreadEventId?.let { + EventEntity + .where(realm, it) + .findFirst() + } + +/** + * Mark or update the current event a root thread event + */ +internal fun EventEntity.markEventAsRoot(threadsCounted: Int, + latestMessageTimelineEventEntity: TimelineEventEntity?) { + isRootThread = true + numberOfThreads = threadsCounted + threadSummaryLatestMessage = latestMessageTimelineEventEntity +} + +/** + * Find all TimelineEventEntity that are threads bind to the Event with rootThreadEventId + * @param rootThreadEventId The root eventId that will try to find bind threads + */ +internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEventId: String): RealmResults = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + + + 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 21a93ba904..de4be16493 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 @@ -24,6 +24,9 @@ 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.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.threads.ThreadDetails +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.di.MoshiProvider @@ -41,8 +44,6 @@ 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 @@ -55,6 +56,9 @@ internal object EventMapper { } eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorCode = event.mCryptoError?.name + eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false + eventEntity.rootThreadEventId = event.getRootThreadEventId() + eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0 return eventEntity } @@ -97,7 +101,20 @@ internal object EventMapper { MXCryptoError.ErrorType.valueOf(errorCode) } it.mCryptoErrorReason = eventEntity.decryptionErrorReason - it.isRootThread = eventEntity.isRootThread() + + it.threadDetails = ThreadDetails( + isRootThread = eventEntity.isRootThread, + numberOfThreads = eventEntity.numberOfThreads, + threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity -> + SenderInfo( + userId = timelineEventEntity.root?.sender ?: "", + displayName = timelineEventEntity.senderName, + isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, + avatarUrl = timelineEventEntity.senderAvatar + ) + }, + threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedMessageText().orEmpty() + ) } } } 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 2b763dd941..0b9a1ee8cc 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,7 +27,6 @@ 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, @@ -46,7 +45,6 @@ internal open class ChunkEntity(@Index var prevToken: String? = 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 ad889dd352..1898d63af8 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,15 +27,13 @@ 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, @@ -43,7 +41,13 @@ internal open class EventEntity(@Index var eventId: String = "", var decryptionResultJson: String? = null, var decryptionErrorCode: String? = null, var decryptionErrorReason: String? = null, - var ageLocalTs: Long? = null + var ageLocalTs: Long? = null, + // Thread related, no need to create a new Entity for performance + @Index var isRootThread: Boolean = false, + @Index var rootThreadEventId: String? = null, + var numberOfThreads: Int = 0, + var threadSummaryLatestMessage: TimelineEventEntity? = null + ) : RealmObject() { private var sendStateStr: String = SendState.UNKNOWN.name @@ -78,9 +82,6 @@ internal open class EventEntity(@Index var eventId: String = "", ?.canBeProcessed = true } - /** - * Returns true if the current event is a thread root event - */ - fun isRootThread(): Boolean = isThread && rootThreadEventId == null + fun isThread(): Boolean = 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 6018305c39..2261d9786a 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,11 +33,9 @@ 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() } @@ -45,15 +43,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() } @@ -72,16 +70,3 @@ 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/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 240b2a0691..e27130442d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -85,3 +85,8 @@ internal fun RealmList.find(eventId: String): EventEntity? { internal fun RealmList.fastContains(eventId: String): Boolean { return this.find(eventId) != null } + +internal fun EventEntity.Companion.whereRootThreadEventId(realm: Realm, rootThreadEventId: String): RealmQuery { + return realm.where() + .equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index aa1ce41bb7..9ce59904b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -25,9 +25,11 @@ import io.realm.kotlin.where import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import timber.log.Timber internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() @@ -59,6 +61,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters) + val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters) val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { sendingTimelineEvents @@ -100,6 +103,7 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent if (filters.filterRedacted) { not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) } + return this } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0c917448cc..a8a72d8a52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -101,7 +101,7 @@ internal class DefaultTimeline( private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsState = AtomicReference(TimelineState()) private val forwardsState = AtomicReference(TimelineState()) - + private var isFromThreadTimeline = false override val timelineID = UUID.randomUUID().toString() override val isLive @@ -143,8 +143,9 @@ internal class DefaultTimeline( } } - override fun start() { + override fun start(rootThreadEventId: String?) { if (isStarted.compareAndSet(false, true)) { + isFromThreadTimeline = rootThreadEventId != null Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") timelineInput.listeners.add(this) BACKGROUND_HANDLER.post { @@ -163,7 +164,13 @@ internal class DefaultTimeline( postSnapshot() } - timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + timelineEvents = rootThreadEventId?.let { + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + timelineEvents.addChangeListener(eventsChangeListener) handleInitialLoad() loadRoomMembersTask @@ -313,16 +320,18 @@ internal class DefaultTimeline( val firstCacheEvent = results.firstOrNull() val chunkEntity = getLiveChunk() + updateState(Timeline.Direction.FORWARDS) { it.copy( - hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastForward ?: false + hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), // what is in DB + hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more ) } updateState(Timeline.Direction.BACKWARDS) { + it.copy( hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE + hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE ) } } @@ -472,6 +481,7 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it accesses realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { + val currentChunk = getLiveChunk() val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken if (token == null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt index 8aeccb66c8..cb23061eda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber import javax.inject.Inject internal interface PaginationTask : Task { 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 7b873166b4..ba34e88ff7 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 @@ -19,6 +19,7 @@ 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.BuildConfig 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 @@ -28,10 +29,10 @@ import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addStateEvent 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.helper.updateThreadSummaryIfNeeded 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 @@ -41,9 +42,9 @@ 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.database.query.whereRootThreadEventId import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper import org.matrix.android.sdk.internal.util.awaitTransaction @@ -160,6 +161,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } } + return if (receivedChunk.events.isEmpty()) { if (receivedChunk.hasMore()) { Result.SHOULD_FETCH_MORE @@ -210,6 +212,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } } val eventIds = ArrayList(eventList.size) + + val optimizedThreadSummaryMap = hashMapOf() eventList.forEach { event -> if (event.eventId == null || event.senderId == null) { return@forEach @@ -226,16 +230,18 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } - Timber.i("------> [TokenChunkEventPersistor] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}") + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) + + eventEntity.rootThreadEventId?.let { + // This is a thread event + optimizedThreadSummaryMap[it] = eventEntity + } ?: run { + // This is a normal event or a root thread one + optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity + } - 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) Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") @@ -254,49 +260,63 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) if (shouldUpdateSummary) { + // TODO maybe add support to view latest thread message roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } if (currentChunk.isValid) { RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } + + optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() + } - /** - * 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) - } - } +// /** +// * Mark or update the thread root event accordingly. If the Threading is disabled +// * no action is done +// */ +// private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) { +// +// if (!BuildConfig.THREADING_ENABLED) return +// +// val rootThreadEventId = eventEntity.rootThreadEventId +// +// if (eventEntity.isThread && rootThreadEventId != null) { +// markEventAsRootEvent(realm, rootThreadEventId) +// } else { +// markAsRootEventIfNeeded(realm, eventEntity.eventId) +// } +// } - private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) { - val rootThreadEvent = EventEntity - .where(realm, rootThreadEventId) - .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return - rootThreadEvent.isThread = true - } +// /** +// * Finds the event with rootThreadEventId and marks it as a root thread +// */ +// private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) { +// val rootThreadEvent = EventEntity +// .where(realm, rootThreadEventId) +// .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return +// rootThreadEvent.isThread = true +// } +// +// /** +// * Also check if there is at least one thread message for that rootThreadEventId, +// * that means it is a root thread so it should be updated accordingly +// */ +// private fun markAsRootEventIfNeeded(realm: Realm, candidateIdRootThread: String) { +// EventEntity +// .whereRootThreadEventId(realm, candidateIdRootThread) +// .findFirst() ?: return +// +// markEventAsRootEvent(realm, candidateIdRootThread) +// } - /** - * 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 - } - } +// /** +// * 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 ea5aedeee9..8d64c7fc96 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,41 +21,33 @@ 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.helper.updateThreadSummaryIfNeeded 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 @@ -68,11 +60,8 @@ 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 @@ -357,11 +346,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle syncLocalTimestampMillis: Long, aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + val chunkEntity = if (!isLimited && lastChunk != null) { + // There are no more events to fetch lastChunk } else { realm.createObject().apply { this.prevToken = prevToken } } + // Only one chunk has isLastForward set to true lastChunk?.isLastForward = false chunkEntity.isLastForward = true @@ -369,21 +361,7 @@ 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") -///////////////////// + val optimizedThreadSummaryMap = hashMapOf() for (event in eventList) { if (event.eventId == null || event.senderId == null || event.type == null) { continue @@ -413,15 +391,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle rootStateEvent?.asDomain()?.getFixedRoomMemberContent() } - 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) + chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) + eventEntity.rootThreadEventId?.let { + // This is a thread event + optimizedThreadSummaryMap[it] = eventEntity + } ?: run { + // This is a normal event or a root thread one + optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity + } // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) @@ -447,56 +424,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } } + + optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() + // 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/build.gradle b/vector/build.gradle index d06779d61c..7fae950d04 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -159,6 +159,9 @@ android { // This *must* only be set in trusted environments. buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false" + // Indicates whether or not threading support is enabled + buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk 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 bbccc78c9c..ecc96e4be8 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 @@ -359,7 +359,6 @@ class RoomDetailFragment @Inject constructor( } else { setupToolbar(views.roomToolbar) } - setupThreadIfNeeded() setupRecyclerView() setupComposer() setupNotificationView() @@ -1194,12 +1193,6 @@ 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 @@ -1762,7 +1755,7 @@ class RoomDetailFragment @Inject constructor( this.view?.hideKeyboard() MessageActionsBottomSheet - .newInstance(roomId, informationData) + .newInstance(roomId, informationData, isThreadTimeLine()) .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") return true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index b9622ae9ae..28b5e88a82 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -160,7 +160,7 @@ class RoomDetailViewModel @AssistedInject constructor( } init { - timeline.start() + timeline.start(initialState.rootThreadEventId) timeline.addListener(this) observeRoomSummary() observeMembershipChanges() @@ -1094,6 +1094,7 @@ class RoomDetailViewModel @AssistedInject constructor( } override fun onTimelineUpdated(snapshot: List) { + timelineEvents.tryEmit(snapshot) // PreviewUrl 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 3266ae60e4..1848f1b28e 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 @@ -73,7 +73,8 @@ data class RoomDetailViewState( roomId = args.roomId, eventId = args.eventId, // Also highlight the target event, if any - highlightedEventId = args.eventId + highlightedEventId = args.eventId, + rootThreadEventId = args.roomThreadDetailArgs?.eventId ) fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 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 1541d5738b..158bc85cb1 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 @@ -89,8 +89,6 @@ class TextComposerViewModel @AssistedInject constructor( is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) - is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action) - } } @@ -98,10 +96,6 @@ 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 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 36bdc4f5b2..f4dd5adebe 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 @@ -53,7 +53,9 @@ data class TextComposerViewState( val isComposerVisible: Boolean get() = canSendMessage && !isVoiceRecording - constructor(args: RoomDetailArgs) : this(roomId = args.roomId) + constructor(args: RoomDetailArgs) : this( + roomId = args.roomId, + rootThreadEventId = args.roomThreadDetailArgs?.eventId) fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index d320f0b6e0..d08259d739 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -93,14 +93,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val unreadState: UnreadState = UnreadState.Unknown, val highlightedEventId: String? = null, val jitsiState: JitsiState = JitsiState(), - val roomSummary: RoomSummary? = null + val roomSummary: RoomSummary? = null, + val rootThreadEventId: String? = null ) { constructor(state: RoomDetailViewState) : this( unreadState = state.unreadState, highlightedEventId = state.highlightedEventId, jitsiState = state.jitsiState, - roomSummary = state.asyncRoomSummary() + roomSummary = state.asyncRoomSummary(), + rootThreadEventId = state.rootThreadEventId ) } @@ -191,7 +193,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // it's sent by the same user so we are sure we have up to date information. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { - timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId ) } if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { modelCache[prevDisplayableEventIndex] = null @@ -319,6 +321,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun submitSnapshot(newSnapshot: List) { + // Update is triggered on any DB change backgroundHandler.post { inSubmitList = true val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) @@ -367,7 +370,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val nextEvent = currentSnapshot.nextOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position) val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { - timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId) } // Should be build if not cached or if model should be refreshed if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { @@ -449,7 +452,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return null } // If the event is not shown, we go to the next one - if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { + if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) { continue } // If the event is sent by us, we update the holder with the eventId and stop the search @@ -471,7 +474,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId } - if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { + if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) { lastShownEventId = event.eventId } if (lastShownEventId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index c1c145040e..0cf7e60eae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -49,10 +49,15 @@ data class MessageActionState( // For actions val actions: List = emptyList(), val expendedReportContentMenu: Boolean = false, - val actionPermissions: ActionPermissions = ActionPermissions() + val actionPermissions: ActionPermissions = ActionPermissions(), + val isFromThreadTimeline: Boolean = false ) : MavericksState { - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) + constructor(args: TimelineEventFragmentArgs) : this( + roomId = args.roomId, + eventId = args.eventId, + informationData = args.informationData, + isFromThreadTimeline = args.isFromThreadTimeline) fun senderName(): String = informationData.memberName?.toString() ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 6de8864f10..3aad4f1e7e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -97,13 +97,14 @@ class MessageActionsBottomSheet : } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet { + fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean): MessageActionsBottomSheet { return MessageActionsBottomSheet().apply { setArguments( TimelineEventFragmentArgs( informationData.eventId, roomId, - informationData + informationData, + isFromThreadTimeline ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index bdd5177058..6762ed1479 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -22,6 +22,7 @@ import dagger.Lazy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.canReact @@ -326,7 +327,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Reply(eventId)) } - // *** Testing Threads **** if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { add(EventSharedAction.ReplyInThread(eventId)) } @@ -417,18 +417,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun canReplyInThread(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { + private fun canReplyInThread(event: TimelineEvent, + messageContent: MessageContent?, + actionPermissions: ActionPermissions): Boolean { // Only event of type EventType.MESSAGE are supported for the moment + if (!BuildConfig.THREADING_ENABLED) return false + if (initialState.isFromThreadTimeline) return false if (event.root.getClearType() != EventType.MESSAGE) return false if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_FILE -> true + MessageType.MSGTYPE_TEXT -> true +// MessageType.MSGTYPE_NOTICE, +// MessageType.MSGTYPE_EMOTE, +// MessageType.MSGTYPE_IMAGE, +// MessageType.MSGTYPE_VIDEO, +// MessageType.MSGTYPE_AUDIO, +// MessageType.MSGTYPE_FILE -> true else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt index 1bb1a876bd..2bd3c54d52 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt @@ -24,5 +24,6 @@ import kotlinx.parcelize.Parcelize data class TimelineEventFragmentArgs( val eventId: String, val roomId: String, - val informationData: MessageInformationData + val informationData: MessageInformationData, + val isFromThreadTimeline: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index e378969b4a..fa699f0c78 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { - val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight) + val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.rootThreadEventId) return if (mergedEvents.isEmpty()) { null } else { 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 54cdb6db09..0ee28404df 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, event.root.isRootThread) + val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, event.root.threadDetails) // val all = event.root.toContent() // val ev = all.toModel() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index c21fe935bb..96786e3377 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -42,8 +42,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> { val event = params.event val computedModel = try { - if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) { - return buildEmptyItem(event, params.prevEvent, params.highlightedEventId) + if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.rootThreadEventId)) { + return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId) } when (event.root.getClearType()) { // Message itemsX @@ -109,11 +109,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me Timber.e(throwable, "failed to create message item") defaultItemFactory.create(params, throwable) } - return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId) + return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId) } - private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem { - val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId) + private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, rootThreadEventId: String?): TimelineEmptyItem { + val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, rootThreadEventId) return TimelineEmptyItem_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index cdfedb2925..94e94911c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -34,5 +34,8 @@ data class TimelineItemFactoryParams( val highlightedEventId: String? get() = partialState.highlightedEventId + val rootThreadEventId: String? + get() = partialState.rootThreadEventId + val isHighlighted = highlightedEventId == event.eventId } 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 80b36fa69f..11061cbc9a 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 @@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import org.matrix.android.sdk.api.session.threads.ThreadDetails import javax.inject.Inject class MessageItemAttributesFactory @Inject constructor( @@ -32,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor( fun create(messageContent: Any?, informationData: MessageInformationData, callback: TimelineEventController.Callback?, - isRootThread: Boolean = false): AbsMessageItem.Attributes { + threadDetails: ThreadDetails? = null): AbsMessageItem.Attributes { return AbsMessageItem.Attributes( avatarSize = avatarSizeProvider.avatarSize, informationData = informationData, @@ -51,7 +52,7 @@ class MessageItemAttributesFactory @Inject constructor( avatarCallback = callback, readReceiptsCallback = callback, emojiTypeFace = emojiCompatFontProvider.typeface, - isRootThread = isRootThread + threadDetails = threadDetails ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 580d7d18cf..c56e9d1336 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -18,9 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.extensions.localDateTime import im.vector.app.core.resources.UserPreferencesProvider +import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent +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.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent @@ -37,7 +40,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * * @return a list of timeline events which have sequentially the same type following the next direction. */ - fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + private fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List { if (index >= timelineEvents.size - 1) { return emptyList() } @@ -59,7 +62,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else { nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) } - val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight) } + val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, rootThreadEventId) } if (filteredSameTypeEvents.size < minSize) { return emptyList() } @@ -74,12 +77,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * * @return a list of timeline events which have sequentially the same type following the prev direction. */ - fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List { val prevSub = timelineEvents.subList(0, index + 1) return prevSub .reversed() .let { - nextSameTypeEvents(it, 0, minSize, eventIdToHighlight) + nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId) } } @@ -88,7 +91,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * @param highlightedEventId can be checked to force visibility to true * @return true if the event should be shown in the timeline. */ - fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?): Boolean { + fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, rootThreadEventId: String?): Boolean { // If show hidden events is true we should always display something if (userPreferencesProvider.shouldShowHiddenEvents()) { return true @@ -100,15 +103,16 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen if (!timelineEvent.isDisplayable()) { return false } + // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences. - return !timelineEvent.shouldBeHidden() + return !timelineEvent.shouldBeHidden(rootThreadEventId) } private fun TimelineEvent.isDisplayable(): Boolean { return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) } - private fun TimelineEvent.shouldBeHidden(): Boolean { + private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?): Boolean { if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { return true } @@ -120,6 +124,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true } + + if(BuildConfig.THREADING_ENABLED && rootThreadEventId == null && root.isThread() && root.getRootThreadEventId() != null){ + return true + } + return false } 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 f6672a1d7c..0649755c2a 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 @@ -18,10 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.item import android.graphics.Typeface import android.view.View +import android.view.ViewStub import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -32,6 +34,9 @@ import im.vector.app.core.ui.views.SendStateImageView import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import org.matrix.android.sdk.api.session.threads.ThreadDetails +import org.matrix.android.sdk.api.util.MatrixItem +import timber.log.Timber /** * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state @@ -98,9 +103,20 @@ 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 + + // Threads + attributes.threadDetails?.let { threadDetails -> + threadDetails.isRootThread + holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread + holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() + holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage + threadDetails.threadSummarySenderInfo?.let { senderInfo -> + attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) + } + } } + override fun unbind(holder: H) { attributes.avatarRenderer.clear(holder.avatarImageView) holder.avatarImageView.setOnClickListener(null) @@ -118,7 +134,11 @@ 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) + val threadSummaryConstraintLayout by bind(R.id.messageThreadSummaryConstraintLayout) + val threadSummaryCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) + val threadSummaryImageView by bind(R.id.messageThreadSummaryImageView) + val threadSummaryAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) + val threadSummaryInfoTextView by bind(R.id.messageThreadSummaryInfoTextView) } /** @@ -136,7 +156,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem val avatarCallback: TimelineEventController.AvatarCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null, - val isRootThread: Boolean = false + val threadDetails: ThreadDetails? = null ) : AbsBaseMessageItem.Attributes { // Have to override as it's used to diff epoxy items @@ -148,6 +168,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem if (avatarSize != other.avatarSize) return false if (informationData != other.informationData) return false + if (threadDetails != other.threadDetails) return false return true } @@ -155,6 +176,8 @@ abstract class AbsMessageItem : AbsBaseMessageItem override fun hashCode(): Int { var result = avatarSize result = 31 * result + informationData.hashCode() + result = 31 * result + threadDetails.hashCode() + return result } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt index 0d5dbc5a8e..1a90951f07 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt @@ -110,7 +110,7 @@ class MergedTimelines( secondaryTimeline.removeAllListeners() } - override fun start() { + override fun start(rootThreadEventId: String?) { mainTimeline.start() secondaryTimeline.start() } diff --git a/vector/src/main/res/drawable/ic_reply_in_thread.xml b/vector/src/main/res/drawable/ic_reply_in_thread.xml index 955dc27f45..3b9b595bd3 100644 --- a/vector/src/main/res/drawable/ic_reply_in_thread.xml +++ b/vector/src/main/res/drawable/ic_reply_in_thread.xml @@ -1,24 +1,8 @@ - - - - - - - - - - - \ No newline at end of file + + + + + diff --git a/vector/src/main/res/drawable/ic_thread_summary.xml b/vector/src/main/res/drawable/ic_thread_summary.xml new file mode 100644 index 0000000000..5e27ad0a0a --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_summary.xml @@ -0,0 +1,11 @@ + + + + + + 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 f9d4314813..a1e1827d52 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -33,8 +33,8 @@ android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="4dp" - android:layout_toStartOf="@+id/messageTimeView" - android:layout_toEndOf="@+id/messageStartGuideline" + android:layout_toStartOf="@id/messageTimeView" + android:layout_toEndOf="@id/messageStartGuideline" android:ellipsize="end" android:maxLines="1" android:textColor="?vctr_content_primary" @@ -200,17 +200,7 @@ - + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_room_summary.xml b/vector/src/main/res/layout/view_thread_room_summary.xml new file mode 100644 index 0000000000..31bdd5ce06 --- /dev/null +++ b/vector/src/main/res/layout/view_thread_room_summary.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + From 4160688f83b37479940fdad985fe69d71e8ab6d6 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 16 Nov 2021 14:59:30 +0200 Subject: [PATCH 007/130] Supporting command in threads --- .../room/model/relation/RelationService.kt | 7 +- .../room/relation/DefaultRelationService.kt | 7 +- .../room/send/LocalEchoEventFactory.kt | 22 ++- .../command/AutocompleteCommandPresenter.kt | 25 ++- .../im/vector/app/features/command/Command.kt | 72 ++++---- .../app/features/command/CommandParser.kt | 17 +- .../app/features/command/ParsedCommand.kt | 2 + .../home/room/detail/AutoCompleter.kt | 9 +- .../home/room/detail/RoomDetailFragment.kt | 28 ++-- .../detail/composer/TextComposerViewEvents.kt | 1 + .../detail/composer/TextComposerViewModel.kt | 158 +++++++++++------- vector/src/main/res/values/strings.xml | 1 + 12 files changed, 225 insertions(+), 124 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 226769ced4..be6d1d9aa3 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 @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional @@ -136,6 +137,8 @@ interface RelationService { * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present */ fun replyInThread(rootThreadEventId: String, - replyInThreadText: CharSequence, - autoMarkdown: Boolean = false): Cancelable? + replyInThreadText: CharSequence, + msgType: String = MessageType.MSGTYPE_TEXT, + autoMarkdown: Boolean = false, + formattedText: String? = null): Cancelable? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 833f056ceb..2184e83ac5 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,12 +159,15 @@ internal class DefaultRelationService @AssistedInject constructor( } } - override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable { + override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String, autoMarkdown: Boolean, formattedText: String?): Cancelable { val event = eventFactory.createThreadTextEvent( rootThreadEventId = rootThreadEventId, roomId = roomId, text = replyInThreadText.toString(), - autoMarkdown = autoMarkdown) + msgType = msgType, + autoMarkdown = autoMarkdown, + formattedText = formattedText + ) // .also { // saveLocalEcho(it) // } 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 b69e868338..7d99dc67bf 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 @@ -343,13 +343,21 @@ internal class LocalEchoEventFactory @Inject constructor( /** * Creates a thread event related to the already existing event */ - fun createThreadTextEvent(rootThreadEventId: String, roomId:String, text: String, autoMarkdown: Boolean): Event = - createEvent( - roomId, - EventType.MESSAGE, - createTextContent(text, autoMarkdown) - .toThreadTextContent(rootThreadEventId) - .toContent()) + fun createThreadTextEvent( + rootThreadEventId: String, + roomId: String, + text: String, + msgType: String, + autoMarkdown: Boolean, + formattedText: String?): Event { + + val content = formattedText?.let { TextContent(text, it) } ?: createTextContent(text, autoMarkdown) + return createEvent( + roomId, + EventType.MESSAGE, + content.toThreadTextContent(rootThreadEventId, msgType) + .toContent()) + } private fun dummyOriginServerTs(): Long { return System.currentTimeMillis() diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt index 5ad31aeaa6..7846ebab37 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -18,17 +18,30 @@ package im.vector.app.features.autocomplete.command import android.content.Context import androidx.recyclerview.widget.RecyclerView +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.BuildConfig import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.command.Command +import im.vector.app.features.home.room.detail.AutoCompleter import im.vector.app.features.settings.VectorPreferences +import timber.log.Timber import javax.inject.Inject -class AutocompleteCommandPresenter @Inject constructor(context: Context, - private val controller: AutocompleteCommandController, - private val vectorPreferences: VectorPreferences) : +class AutocompleteCommandPresenter @AssistedInject constructor( + @Assisted val isInThreadTimeline: Boolean, + context: Context, + private val controller: AutocompleteCommandController, + private val vectorPreferences: VectorPreferences) : RecyclerViewPresenter(context), AutocompleteClickListener { + @AssistedFactory + interface Factory { + fun create(isFromThreadTimeline: Boolean): AutocompleteCommandPresenter + } + init { controller.listener = this } @@ -46,6 +59,12 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, .filter { !it.isDevCommand || vectorPreferences.developerMode() } + .filter { + if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) { + it.isThreadCommand + } else + true + } .filter { if (query.isNullOrEmpty()) { true diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 33ccd08d22..2c0d6e8387 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -24,42 +24,42 @@ import im.vector.app.R * the user can write theses messages to perform some actions * the list will be displayed in this order */ -enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean) { - EMOTE("/me", "", R.string.command_description_emote, false), - BAN_USER("/ban", " [reason]", R.string.command_description_ban_user, false), - UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user, false), - IGNORE_USER("/ignore", " [reason]", R.string.command_description_ignore_user, false), - UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user, false), - SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user, false), - RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false), - ROOM_NAME("/roomname", "", R.string.command_description_room_name, false), - INVITE("/invite", " [reason]", R.string.command_description_invite_user, false), - JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), - PART("/part", " [reason]", R.string.command_description_part_room, false), - TOPIC("/topic", "", R.string.command_description_topic, false), - KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false), - CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false), - CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room, false), - ROOM_AVATAR("/roomavatar", "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */), - CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */), - MARKDOWN("/markdown", "", R.string.command_description_markdown, false), - RAINBOW("/rainbow", "", R.string.command_description_rainbow, false), - RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false), - CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false), - SPOILER("/spoiler", "", R.string.command_description_spoiler, false), - POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false), - SHRUG("/shrug", "", R.string.command_description_shrug, false), - LENNY("/lenny", "", R.string.command_description_lenny, false), - PLAIN("/plain", "", R.string.command_description_plain, false), - WHOIS("/whois", "", R.string.command_description_whois, false), - DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), - CONFETTI("/confetti", "", R.string.command_confetti, false), - SNOWFALL("/snowfall", "", R.string.command_snow, false), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true), - JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), - LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), - UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); +enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean, val isThreadCommand: Boolean) { + EMOTE("/me", "", R.string.command_description_emote, false, true), + BAN_USER("/ban", " [reason]", R.string.command_description_ban_user, false, false), + UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user, false, false), + IGNORE_USER("/ignore", " [reason]", R.string.command_description_ignore_user, false, true), + UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user, false, true), + SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user, false, false), + RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false, false), + ROOM_NAME("/roomname", "", R.string.command_description_room_name, false, false), + INVITE("/invite", " [reason]", R.string.command_description_invite_user, false, false), + JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false, false), + PART("/part", " [reason]", R.string.command_description_part_room, false, false), + TOPIC("/topic", "", R.string.command_description_topic, false, false), + KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false, false), + CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false, false), + CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room, false, false), + ROOM_AVATAR("/roomavatar", "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */, false), + CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */, false), + MARKDOWN("/markdown", "", R.string.command_description_markdown, false, false), + RAINBOW("/rainbow", "", R.string.command_description_rainbow, false, true), + RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false, true), + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false, false), + SPOILER("/spoiler", "", R.string.command_description_spoiler, false, true), + POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll, false, false), + SHRUG("/shrug", "", R.string.command_description_shrug, false, true), + LENNY("/lenny", "", R.string.command_description_lenny, false, true), + PLAIN("/plain", "", R.string.command_description_plain, false, true), + WHOIS("/whois", "", R.string.command_description_whois, false, true), + DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false, false), + CONFETTI("/confetti", "", R.string.command_confetti, false, false), + SNOWFALL("/snowfall", "", R.string.command_snow, false, false), + CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true, false), + ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true, false), + JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true, false), + LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true, false), + UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true, false); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index e570033d35..47dbce1376 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -16,6 +16,7 @@ package im.vector.app.features.command +import im.vector.app.BuildConfig import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.features.home.room.detail.ChatEffect @@ -32,7 +33,7 @@ object CommandParser { * @param textMessage the text message * @return a parsed slash command (ok or error) */ - fun parseSplashCommand(textMessage: CharSequence): ParsedCommand { + fun parseSplashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand { // check if it has the Slash marker if (!textMessage.startsWith("/")) { return ParsedCommand.ErrorNotACommand @@ -61,6 +62,20 @@ object CommandParser { return ParsedCommand.ErrorEmptySlashCommand } + // If the command is not supported by threads return error + + if(BuildConfig.THREADING_ENABLED && isInThreadTimeline){ + val slashCommand = messageParts.first() + val notSupportedCommandsInThreads = Command.values().filter { + !it.isThreadCommand + }.map { + it.command + } + if(notSupportedCommandsInThreads.contains(slashCommand)){ + return ParsedCommand.ErrorCommandNotSupportedInThreads(slashCommand) + } + } + return when (val slashCommand = messageParts.first()) { Command.PLAIN.command -> { val text = textMessage.substring(Command.PLAIN.command.length).trim() diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index bafb9153e6..a439b3eb46 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -28,6 +28,8 @@ sealed class ParsedCommand { object ErrorEmptySlashCommand : ParsedCommand() + class ErrorCommandNotSupportedInThreads(val slashCommand: String) : ParsedCommand() + // Unknown/Unsupported slash command class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt index 1d6530218d..053b267a5d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt @@ -49,9 +49,10 @@ import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem class AutoCompleter @AssistedInject constructor( @Assisted val roomId: String, + @Assisted val isInThreadTimeline: Boolean, private val avatarRenderer: AvatarRenderer, private val commandAutocompletePolicy: CommandAutocompletePolicy, - private val autocompleteCommandPresenter: AutocompleteCommandPresenter, + AutocompleteCommandPresenterFactory: AutocompleteCommandPresenter.Factory, private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter, @@ -62,7 +63,11 @@ class AutoCompleter @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(roomId: String): AutoCompleter + fun create(roomId: String, isInThreadTimeline: Boolean): AutoCompleter + } + + private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy { + AutocompleteCommandPresenterFactory.create(isInThreadTimeline) } private var editText: EditText? = null 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 ecc96e4be8..6dfe29cec6 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 @@ -291,8 +291,9 @@ class RoomDetailFragment @Inject constructor( } private val autoCompleter: AutoCompleter by lazy { - autoCompleterFactory.create(roomDetailArgs.roomId) + autoCompleterFactory.create(roomDetailArgs.roomId, isThreadTimeLine()) } + private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val debouncer = Debouncer(createUIHandler()) @@ -396,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) } } @@ -1466,24 +1467,27 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: TextComposerViewEvents.SendMessageResult) { when (sendMessageResult) { - is TextComposerViewEvents.SlashCommandHandled -> { + is TextComposerViewEvents.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } - is TextComposerViewEvents.SlashCommandError -> { + is TextComposerViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is TextComposerViewEvents.SlashCommandUnknown -> { + is TextComposerViewEvents.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is TextComposerViewEvents.SlashCommandResultOk -> { + is TextComposerViewEvents.SlashCommandResultOk -> { views.composerLayout.setTextIfDifferent("") } - is TextComposerViewEvents.SlashCommandResultError -> { + is TextComposerViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } - is TextComposerViewEvents.SlashCommandNotImplemented -> { + is TextComposerViewEvents.SlashCommandNotImplemented -> { displayCommandError(getString(R.string.not_implemented)) } + is TextComposerViewEvents.SlashCommandNotSupportedInThreads -> { + displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command)) + } } // .exhaustive lockSendButton = false @@ -2217,6 +2221,6 @@ class RoomDetailFragment @Inject constructor( } } - fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null + private 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/composer/TextComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt index 691ed4d93e..e40e2b0b83 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewEvents.kt @@ -32,6 +32,7 @@ sealed class TextComposerViewEvents : VectorViewEvents { data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() + class SlashCommandNotSupportedInThreads(val command: String) : SendMessageResult() data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() object SlashCommandResultOk : SendMessageResult() class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() 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 158bc85cb1..1fa1bfde35 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 @@ -152,160 +152,198 @@ class TextComposerViewModel @AssistedInject constructor( private fun handleSendMessage(action: TextComposerAction.SendMessage) { withState { state -> when (state.sendMode) { - is SendMode.REGULAR -> { - when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { - is ParsedCommand.ErrorNotACommand -> { + is SendMode.REGULAR -> { + when (val slashCommandResult = CommandParser.parseSplashCommand(action.text, state.isInThreadTimeline())) { + is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room if (state.rootThreadEventId != null) - room.replyInThread(state.rootThreadEventId, action.text.toString(), action.autoMarkdown) + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = action.text.toString(), + autoMarkdown = action.autoMarkdown) else room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } - is ParsedCommand.ErrorSyntax -> { + is ParsedCommand.ErrorSyntax -> { _viewEvents.post(TextComposerViewEvents.SlashCommandError(slashCommandResult.command)) } - is ParsedCommand.ErrorEmptySlashCommand -> { + is ParsedCommand.ErrorEmptySlashCommand -> { _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown("/")) } - is ParsedCommand.ErrorUnknownSlashCommand -> { + is ParsedCommand.ErrorUnknownSlashCommand -> { _viewEvents.post(TextComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) } - is ParsedCommand.SendPlainText -> { + is ParsedCommand.ErrorCommandNotSupportedInThreads -> { + _viewEvents.post(TextComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.slashCommand)) + } + is ParsedCommand.SendPlainText -> { // Send the text message to the room, without markdown - room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) + if (state.rootThreadEventId != null) + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = action.text.toString(), + autoMarkdown = false) + else + room.sendTextMessage(slashCommandResult.message, autoMarkdown = false) _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } - is ParsedCommand.ChangeRoomName -> { + is ParsedCommand.ChangeRoomName -> { handleChangeRoomNameSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.Invite -> { + is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.Invite3Pid -> { + is ParsedCommand.Invite3Pid -> { handleInvite3pidSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.SetUserPowerLevel -> { + is ParsedCommand.SetUserPowerLevel -> { handleSetUserPowerLevel(slashCommandResult) popDraft() } - is ParsedCommand.ClearScalarToken -> { + is ParsedCommand.ClearScalarToken -> { // TODO _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) } - is ParsedCommand.SetMarkdown -> { + is ParsedCommand.SetMarkdown -> { vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled( if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) popDraft() } - is ParsedCommand.BanUser -> { + is ParsedCommand.BanUser -> { handleBanSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.UnbanUser -> { + is ParsedCommand.UnbanUser -> { handleUnbanSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.IgnoreUser -> { + is ParsedCommand.IgnoreUser -> { handleIgnoreSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.UnignoreUser -> { + is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.KickUser -> { + is ParsedCommand.KickUser -> { handleKickSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.JoinRoom -> { + is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.PartRoom -> { + is ParsedCommand.PartRoom -> { // TODO _viewEvents.post(TextComposerViewEvents.SlashCommandNotImplemented) } - is ParsedCommand.SendEmote -> { - room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) + is ParsedCommand.SendEmote -> { + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = slashCommandResult.message, + msgType = MessageType.MSGTYPE_EMOTE, + autoMarkdown = action.autoMarkdown) + } ?: room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendRainbow -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) - } + is ParsedCommand.SendRainbow -> { + + val message = slashCommandResult.message.toString() + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = slashCommandResult.message, + formattedText = rainbowGenerator.generate(message)) + } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message)) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendRainbowEmote -> { - slashCommandResult.message.toString().let { - room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) - } + is ParsedCommand.SendRainbowEmote -> { + val message = slashCommandResult.message.toString() + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = slashCommandResult.message, + msgType = MessageType.MSGTYPE_EMOTE, + formattedText = rainbowGenerator.generate(message)) + } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message),MessageType.MSGTYPE_EMOTE) + _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendSpoiler -> { - room.sendFormattedTextMessage( - "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", - "${slashCommandResult.message}" + is ParsedCommand.SendSpoiler -> { + + val text = "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})" + val formattedText = "${slashCommandResult.message}" + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = state.rootThreadEventId, + replyInThreadText = text, + formattedText = formattedText) + } ?: room.sendFormattedTextMessage( + text, + formattedText ) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendShrug -> { - sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message) + is ParsedCommand.SendShrug -> { + + sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message, state.rootThreadEventId) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendLenny -> { - sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message) + is ParsedCommand.SendLenny -> { + sendPrefixedMessage("( ͡° ͜ʖ ͡°)", slashCommandResult.message, state.rootThreadEventId) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendChatEffect -> { + is ParsedCommand.SendChatEffect -> { sendChatEffect(slashCommandResult) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendPoll -> { + is ParsedCommand.SendPoll -> { room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.ChangeTopic -> { + is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.ChangeDisplayName -> { + is ParsedCommand.ChangeDisplayName -> { handleChangeDisplayNameSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.ChangeDisplayNameForRoom -> { + is ParsedCommand.ChangeDisplayNameForRoom -> { handleChangeDisplayNameForRoomSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.ChangeRoomAvatar -> { + is ParsedCommand.ChangeRoomAvatar -> { handleChangeRoomAvatarSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.ChangeAvatarForRoom -> { + is ParsedCommand.ChangeAvatarForRoom -> { handleChangeAvatarForRoomSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.ShowUser -> { + is ParsedCommand.ShowUser -> { _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) handleWhoisSlashCommand(slashCommandResult) popDraft() } - is ParsedCommand.DiscardSession -> { + is ParsedCommand.DiscardSession -> { if (room.isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) @@ -318,7 +356,7 @@ class TextComposerViewModel @AssistedInject constructor( ) } } - is ParsedCommand.CreateSpace -> { + is ParsedCommand.CreateSpace -> { viewModelScope.launch(Dispatchers.IO) { try { val params = CreateSpaceParams().apply { @@ -340,7 +378,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.AddToSpace -> { + is ParsedCommand.AddToSpace -> { viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().getSpace(slashCommandResult.spaceId) @@ -357,7 +395,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.JoinSpace -> { + is ParsedCommand.JoinSpace -> { viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias) @@ -368,7 +406,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.LeaveRoom -> { + is ParsedCommand.LeaveRoom -> { viewModelScope.launch(Dispatchers.IO) { try { session.getRoom(slashCommandResult.roomId)?.leave(null) @@ -379,7 +417,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.UpgradeRoom -> { + is ParsedCommand.UpgradeRoom -> { _viewEvents.post( TextComposerViewEvents.ShowRoomUpgradeDialog( slashCommandResult.newVersion, @@ -391,7 +429,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) { @@ -414,7 +452,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 @@ -435,7 +473,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) @@ -657,7 +695,7 @@ class TextComposerViewModel @AssistedInject constructor( _viewEvents.post(TextComposerViewEvents.OpenRoomMemberProfile(whois.userId)) } - private fun sendPrefixedMessage(prefix: String, message: CharSequence) { + private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { val sequence = buildString { append(prefix) if (message.isNotEmpty()) { @@ -665,7 +703,9 @@ class TextComposerViewModel @AssistedInject constructor( append(message) } } - room.sendTextMessage(sequence) + rootThreadEventId?.let { + room.replyInThread(it, sequence) + }?: room.sendTextMessage(sequence) } /** diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 591cc152b9..b781692733 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1831,6 +1831,7 @@ Command error Unrecognized command: %s The command \"%s\" needs more parameters, or some parameters are incorrect. + The command \"%s\" is recognized but not supported in threads. Displays action Bans user with given id Unbans user with given id From 3d9350091ef44e6284a911c380f43679fd29265f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 17 Nov 2021 13:09:27 +0200 Subject: [PATCH 008/130] Add Replies support from within a thread --- .../room/model/relation/RelationService.kt | 6 +++- .../room/relation/DefaultRelationService.kt | 27 +++++++++++---- .../session/room/relation/EventEditor.kt | 6 +++- .../room/send/LocalEchoEventFactory.kt | 23 ++++++++++--- .../detail/composer/TextComposerAction.kt | 2 -- .../detail/composer/TextComposerViewModel.kt | 34 ++++++++++++++----- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index be6d1d9aa3..a5ecfaf6e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -134,11 +134,15 @@ interface RelationService { * by the sdk into pills. * @param rootThreadEventId the root thread eventId * @param replyInThreadText the reply text + * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param eventReplied the event referenced by the reply within a thread */ fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false, - formattedText: String? = null): Cancelable? + formattedText: String? = null, + eventReplied: TimelineEvent? = null): Cancelable? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 2184e83ac5..23862ae963 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -38,7 +38,6 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.session.room.send.TextContent import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -133,7 +132,11 @@ internal class DefaultRelationService @AssistedInject constructor( } override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { - val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown) + val event = eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = eventReplied, + replyText = replyText, + autoMarkdown = autoMarkdown) ?.also { saveLocalEcho(it) } ?: return null @@ -159,15 +162,27 @@ internal class DefaultRelationService @AssistedInject constructor( } } - override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, msgType: String, autoMarkdown: Boolean, formattedText: String?): Cancelable { - val event = eventFactory.createThreadTextEvent( + override fun replyInThread( + rootThreadEventId: String, + replyInThreadText: CharSequence, + msgType: String, + autoMarkdown: Boolean, + formattedText: String?, + eventReplied: TimelineEvent?): Cancelable { + val event = eventReplied?.let { + eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = eventReplied, + replyText = replyInThreadText, + autoMarkdown = autoMarkdown, + rootThreadEventId = rootThreadEventId) + } ?: eventFactory.createThreadTextEvent( rootThreadEventId = rootThreadEventId, roomId = roomId, text = replyInThreadText.toString(), msgType = msgType, autoMarkdown = autoMarkdown, - formattedText = formattedText - ) + formattedText = formattedText) // .also { // saveLocalEcho(it) // } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index a666d40fc3..7e3d7dfde8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -67,7 +67,11 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: val roomId = replyToEdit.roomId if (replyToEdit.root.sendState.hasFailed()) { // We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event. - val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy( + val editedEvent = eventFactory.createReplyTextEvent( + roomId = roomId, + eventReplied = originalTimelineEvent, + replyText = newBodyText, + autoMarkdown = false)?.copy( eventId = replyToEdit.eventId ) ?: return NoOpCancellable updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 7d99dc67bf..5741d0f5ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -341,7 +341,7 @@ internal class LocalEchoEventFactory @Inject constructor( } /** - * Creates a thread event related to the already existing event + * Creates a thread event related to the already existing root event */ fun createThreadTextEvent( rootThreadEventId: String, @@ -363,10 +363,14 @@ internal class LocalEchoEventFactory @Inject constructor( return System.currentTimeMillis() } + /** + * Creates a reply to a regular timeline Event or a thread Event if needed + */ fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, - autoMarkdown: Boolean): Event? { + autoMarkdown: Boolean, + rootThreadEventId: String? = null): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null @@ -393,11 +397,22 @@ internal class LocalEchoEventFactory @Inject constructor( format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted, - relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) - ) + relatesTo = generateReplyRelationContent(eventId = eventId, rootThreadEventId = rootThreadEventId)) return createMessageEvent(roomId, content) } + /** + * Generates the appropriate relatesTo object for a reply event. + * It can either be a regular reply or a reply within a thread + */ + private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null): RelationDefaultContent = + rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId)) + } ?: RelationDefaultContent(null, null, ReplyToContent(eventId)) + private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { return buildString { append("> <") diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt index 48f6c84983..7725400187 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerAction.kt @@ -28,6 +28,4 @@ sealed class TextComposerAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : TextComposerAction() data class OnTextChanged(val text: CharSequence) : TextComposerAction() data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction() - data class EnterReplyInThreadTimeline(val rootThreadEventId: String) : TextComposerAction() - } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index 1fa1bfde35..bcc26247a2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -276,7 +276,7 @@ class TextComposerViewModel @AssistedInject constructor( replyInThreadText = slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, formattedText = rainbowGenerator.generate(message)) - } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message),MessageType.MSGTYPE_EMOTE) + } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) _viewEvents.post(TextComposerViewEvents.SlashCommandHandled()) popDraft() @@ -465,20 +465,36 @@ class TextComposerViewModel @AssistedInject constructor( val document = parser.parse(finalText) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) + if (finalText == htmlText) { - room.sendTextMessage(finalText) + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = it, + replyInThreadText = finalText) + } ?: room.sendTextMessage(finalText) } else { - room.sendFormattedTextMessage(finalText, htmlText) + state.rootThreadEventId?.let { + room.replyInThread( + rootThreadEventId = it, + replyInThreadText = finalText, + formattedText = htmlText) + } ?: room.sendFormattedTextMessage(finalText, htmlText) } _viewEvents.post(TextComposerViewEvents.MessageSent) popDraft() } is SendMode.REPLY -> { - state.sendMode.timelineEvent.let { - room.replyToMessage(it, action.text.toString(), action.autoMarkdown) - _viewEvents.post(TextComposerViewEvents.MessageSent) - popDraft() - } + val timelineEvent = state.sendMode.timelineEvent + state.rootThreadEventId?.let { rootThreadEventId -> + room.replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = action.text.toString(), + autoMarkdown = action.autoMarkdown, + eventReplied = timelineEvent) + } ?: room.replyToMessage(timelineEvent, action.text.toString(), action.autoMarkdown) + + _viewEvents.post(TextComposerViewEvents.MessageSent) + popDraft() } }.exhaustive } @@ -705,7 +721,7 @@ class TextComposerViewModel @AssistedInject constructor( } rootThreadEventId?.let { room.replyInThread(it, sequence) - }?: room.sendTextMessage(sequence) + } ?: room.sendTextMessage(sequence) } /** From 3de0f7bf373d736b63b9784f76e01d414b8b3551 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 18 Nov 2021 15:48:17 +0200 Subject: [PATCH 009/130] Add sending file to thread support ** Important while this feature depends on local echo, should be added local echo support in threads to work 100% --- .../java/org/matrix/android/sdk/rx/RxRoom.kt | 5 ++- .../sdk/api/session/room/send/SendService.kt | 8 +++-- .../session/room/send/DefaultSendService.kt | 24 +++++++++++--- .../room/send/LocalEchoEventFactory.kt | 33 +++++++++++-------- .../home/room/detail/RoomDetailViewModel.kt | 19 ++++++++--- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index b3495c4493..36f59c0058 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -149,7 +149,10 @@ class RxRoom(private val room: Room) { } fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Completable = rxCompletable { - room.sendMedia(attachment, compressBeforeSending, roomIds) + room.sendMedia( + attachment = attachment, + compressBeforeSending = compressBeforeSending, + roomIds = roomIds) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 6ae42de90c..a1a5d62958 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -63,11 +63,13 @@ interface SendService { * @param compressBeforeSending set to true to compress images before sending them * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread * @return a [Cancelable] */ fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, - roomIds: Set): Cancelable + roomIds: Set, + rootThreadEventId: String? = null): Cancelable /** * Method to send a list of media asynchronously. @@ -75,11 +77,13 @@ interface SendService { * @param compressBeforeSending set to true to compress images before sending them * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. * It can be useful to send media to multiple room. It's safe to include the current roomId in this set + * @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread * @return a [Cancelable] */ fun sendMedias(attachments: List, compressBeforeSending: Boolean, - roomIds: Set): Cancelable + roomIds: Set, + rootThreadEventId: String? = null): Cancelable /** * Send a poll to the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 177c98541c..860940167b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -236,22 +236,38 @@ internal class DefaultSendService @AssistedInject constructor( override fun sendMedias(attachments: List, compressBeforeSending: Boolean, - roomIds: Set): Cancelable { + roomIds: Set, + rootThreadEventId: String? + ): Cancelable { return attachments.mapTo(CancelableBag()) { - sendMedia(it, compressBeforeSending, roomIds) + sendMedia( + attachment = it, + compressBeforeSending = compressBeforeSending, + roomIds = roomIds, + rootThreadEventId = rootThreadEventId) } } override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, - roomIds: Set): Cancelable { + roomIds: Set, + rootThreadEventId: String? + ): Cancelable { + + // Ensure that the event will not be send in a thread if we are a different flow. + // Like sending files to multiple rooms + val rootThreadId = if (roomIds.isNotEmpty()) null else rootThreadEventId + // Create an event with the media file path // Ensure current roomId is included in the set val allRoomIds = (roomIds + roomId).toList() // Create local echo for each room val allLocalEchoes = allRoomIds.map { - localEchoEventFactory.createMediaEvent(it, attachment).also { event -> + localEchoEventFactory.createMediaEvent( + roomId = it, + attachment = attachment, + rootThreadEventId = rootThreadId).also { event -> createLocalEcho(event) } } 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 5741d0f5ba..005f377943 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 @@ -197,12 +197,15 @@ internal class LocalEchoEventFactory @Inject constructor( )) } - fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { + fun createMediaEvent(roomId: String, + attachment: ContentAttachmentData, + rootThreadEventId: String? + ): Event { return when (attachment.type) { - ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) - ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) - ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) - ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId) + ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, rootThreadEventId) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId) } } @@ -225,7 +228,7 @@ internal class LocalEchoEventFactory @Inject constructor( unsignedData = UnsignedData(age = null, transactionId = localId)) } - private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { var width = attachment.width var height = attachment.height @@ -249,12 +252,13 @@ internal class LocalEchoEventFactory @Inject constructor( height = height?.toInt() ?: 0, size = attachment.size ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) } ) return createMessageEvent(roomId, content) } - private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { val mediaDataRetriever = MediaMetadataRetriever() mediaDataRetriever.setDataSource(context, attachment.queryUri) @@ -285,12 +289,13 @@ internal class LocalEchoEventFactory @Inject constructor( thumbnailUrl = attachment.queryUri.toString(), thumbnailInfo = thumbnailInfo ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) } ) return createMessageEvent(roomId, content) } - private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { val isVoiceMessage = attachment.waveform != null val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, @@ -305,12 +310,13 @@ internal class LocalEchoEventFactory @Inject constructor( duration = attachment.duration?.toInt(), waveform = waveformSanitizer.sanitize(attachment.waveform) ), - voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap() + voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(), + relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) } ) return createMessageEvent(roomId, content) } - private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { + private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { val content = MessageFileContent( msgType = MessageType.MSGTYPE_FILE, body = attachment.name ?: "file", @@ -318,7 +324,8 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) } ) return createMessageEvent(roomId, content) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 28b5e88a82..ca7ed4b6ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -531,9 +531,9 @@ class RoomDetailViewModel @AssistedInject constructor( val isAllowed = action.userJustAccepted || if (widget.type == WidgetType.Jitsi) { widget.senderInfo?.userId == session.myUserId || session.integrationManagerService().isNativeWidgetDomainAllowed( - action.widget.type.preferred, - domain - ) + action.widget.type.preferred, + domain + ) } else false if (isAllowed) { @@ -626,7 +626,11 @@ class RoomDetailViewModel @AssistedInject constructor( } else { voiceMessageHelper.stopRecording()?.let { audioType -> if (audioType.duration > 1000) { - room.sendMedia(audioType.toContentAttachmentData(), false, emptySet()) + room.sendMedia( + attachment = audioType.toContentAttachmentData(), + compressBeforeSending = false, + roomIds = emptySet(), + rootThreadEventId = initialState.rootThreadEventId) } else { voiceMessageHelper.deleteRecording() } @@ -705,7 +709,12 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleSendMedia(action: RoomDetailAction.SendMedia) { - room.sendMedias(action.attachments, action.compressBeforeSending, emptySet()) + room.sendMedias( + action.attachments, + action.compressBeforeSending, + emptySet(), + initialState.rootThreadEventId + ) } private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { From 586b3d8caad38bf1ccc14b54dd3129c60d5d4a66 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 22 Nov 2021 14:02:17 +0200 Subject: [PATCH 010/130] - Add specific toolbar for threads - Renamed RoomDetailFragment to TimelineFragment while it should be reused from threads - View root thread message in room functionality --- .../debug/res/layout/fragment_thread_list.xml | 14 ++ vector/src/main/AndroidManifest.xml | 3 +- .../im/vector/app/core/di/FragmentModule.kt | 12 +- .../im/vector/app/core/di/ScreenComponent.kt | 6 +- .../app/features/call/VectorCallActivity.kt | 4 +- .../IncomingVerificationRequestHandler.kt | 4 +- .../home/room/detail/RoomDetailActivity.kt | 17 +- .../home/room/detail/RoomDetailViewModel.kt | 34 ++- .../home/room/detail/RoomDetailViewState.kt | 8 +- ...mDetailFragment.kt => TimelineFragment.kt} | 230 +++++++++++------- .../room/detail/arguments/TimelineArgs.kt | 31 +++ .../detail/composer/TextComposerViewModel.kt | 5 +- .../detail/composer/TextComposerViewState.kt | 6 +- .../timeline/TimelineEventController.kt | 2 +- .../helper/MessageItemAttributesFactory.kt | 2 +- .../home/room/threads/RoomThreadsActivity.kt | 66 ----- .../home/room/threads/ThreadsActivity.kt | 132 ++++++++++ .../room/threads/arguments/ThreadListArgs.kt | 25 ++ .../ThreadTimelineArgs.kt} | 6 +- .../detail/RoomThreadDetailActivity.kt | 95 -------- ...etailFragment.kt => ThreadListFragment.kt} | 19 +- .../features/navigation/DefaultNavigator.kt | 18 +- .../app/features/navigation/Navigator.kt | 4 + .../notifications/NotificationUtils.kt | 6 +- .../res/drawable/ic_thread_link_menu_item.xml | 12 + .../main/res/drawable/ic_thread_menu_item.xml | 10 + .../drawable/ic_thread_share_menu_item.xml | 19 ++ .../ic_thread_view_in_room_menu_item.xml | 30 +++ .../layout/activity_room_thread_detail.xml | 88 ------- .../main/res/layout/activity_room_threads.xml | 88 ------- .../src/main/res/layout/activity_threads.xml | 24 ++ .../main/res/layout/fragment_room_detail.xml | 100 +------- .../layout/fragment_room_thread_detail.xml | 31 --- .../view_room_detail_thread_toolbar.xml | 51 ++++ .../res/layout/view_room_detail_toolbar.xml | 99 ++++++++ .../main/res/menu/menu_thread_timeline.xml | 36 +++ vector/src/main/res/menu/menu_timeline.xml | 47 +++- vector/src/main/res/values/strings.xml | 7 + 38 files changed, 766 insertions(+), 625 deletions(-) create mode 100644 vector/src/debug/res/layout/fragment_thread_list.xml rename vector/src/main/java/im/vector/app/features/home/room/detail/{RoomDetailFragment.kt => TimelineFragment.kt} (93%) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt rename vector/src/main/java/im/vector/app/features/home/room/threads/{detail/arguments/RoomThreadDetailArgs.kt => arguments/ThreadTimelineArgs.kt} (85%) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt rename vector/src/main/java/im/vector/app/features/home/room/threads/detail/{RoomThreadDetailFragment.kt => ThreadListFragment.kt} (74%) create mode 100644 vector/src/main/res/drawable/ic_thread_link_menu_item.xml create mode 100644 vector/src/main/res/drawable/ic_thread_menu_item.xml create mode 100644 vector/src/main/res/drawable/ic_thread_share_menu_item.xml create mode 100644 vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml delete mode 100644 vector/src/main/res/layout/activity_room_thread_detail.xml delete mode 100644 vector/src/main/res/layout/activity_room_threads.xml create mode 100644 vector/src/main/res/layout/activity_threads.xml delete mode 100644 vector/src/main/res/layout/fragment_room_thread_detail.xml create mode 100644 vector/src/main/res/layout/view_room_detail_thread_toolbar.xml create mode 100644 vector/src/main/res/layout/view_room_detail_toolbar.xml create mode 100644 vector/src/main/res/menu/menu_thread_timeline.xml diff --git a/vector/src/debug/res/layout/fragment_thread_list.xml b/vector/src/debug/res/layout/fragment_thread_list.xml new file mode 100644 index 0000000000..cf3a79e776 --- /dev/null +++ b/vector/src/debug/res/layout/fragment_thread_list.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2b7b445ad5..f0e68e8446 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -179,8 +179,7 @@ - - + (), CallContro private fun returnToChat() { val roomId = withState(callViewModel) { it.roomId } - val args = RoomDetailArgs(roomId) + val args = TimelineArgs(roomId) val intent = RoomDetailActivity.newIntent(this, args).apply { flags = FLAG_ACTIVITY_CLEAR_TOP } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 6c009d3786..ea0fb3f0ca 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert import org.matrix.android.sdk.api.session.Session @@ -142,7 +142,7 @@ class IncomingVerificationRequestHandler @Inject constructor( R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { - activity.intent?.extras?.getParcelable(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let { + activity.intent?.extras?.getParcelable(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let { it.roomId != pr.roomId } ?: true } else true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index 76c3816ce6..d9a10d8745 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -34,6 +34,7 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator import im.vector.app.features.room.RequireActiveMembershipAction @@ -102,16 +103,16 @@ class RoomDetailActivity : super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) waitingView = views.waitingView.waitingView - val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { - RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) + val timelineArgs: TimelineArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + TimelineArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) } else { intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) } - if (roomDetailArgs == null) return - currentRoomId = roomDetailArgs.roomId + if (timelineArgs == null) return + currentRoomId = timelineArgs.roomId if (isFirstCreation()) { - replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) + replaceFragment(R.id.roomDetailContainer, TimelineFragment::class.java, timelineArgs) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) } @@ -147,7 +148,7 @@ class RoomDetailActivity : if (currentRoomId != switchToRoom.roomId) { currentRoomId = switchToRoom.roomId requireActiveMembershipViewModel.handle(RequireActiveMembershipAction.ChangeRoom(switchToRoom.roomId)) - replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId)) + replaceFragment(R.id.roomDetailContainer, TimelineFragment::class.java, TimelineArgs(switchToRoom.roomId)) } } @@ -191,9 +192,9 @@ class RoomDetailActivity : const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" - fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent { + fun newIntent(context: Context, timelineArgs: TimelineArgs): Intent { return Intent(context, RoomDetailActivity::class.java).apply { - putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) + putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index ca7ed4b6ec..5854d35fb6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -153,7 +153,7 @@ class RoomDetailViewModel @AssistedInject constructor( @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { - val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: TimelineFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.roomDetailViewModelFactory.create(state) } @@ -668,20 +668,30 @@ class RoomDetailViewModel @AssistedInject constructor( private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> + if (state.asyncRoomSummary()?.membership != Membership.JOIN) { return@withState false } - when (itemId) { - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.open_matrix_apps -> true - R.id.voice_call -> state.isWebRTCCallOptionAvailable() - R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined - // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ - R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> true - R.id.dev_tools -> vectorPreferences.developerMode() - else -> false + + if (initialState.isThreadTimeline()) { + when (itemId) { + R.id.menu_thread_timeline_more -> true + else -> false + } + } else { + when (itemId) { + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.open_matrix_apps -> true + R.id.voice_call -> state.isWebRTCCallOptionAvailable() + R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined + // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ + R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined + R.id.search -> true + R.id.threads -> true + R.id.dev_tools -> vectorPreferences.developerMode() + else -> false + } } } 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 1848f1b28e..fa772ca073 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 @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.initsync.SyncStatusService @@ -69,12 +70,12 @@ data class RoomDetailViewState( val rootThreadEventId: String? = null ) : MavericksState { - constructor(args: RoomDetailArgs) : this( + constructor(args: TimelineArgs) : this( roomId = args.roomId, eventId = args.eventId, // Also highlight the target event, if any highlightedEventId = args.eventId, - rootThreadEventId = args.roomThreadDetailArgs?.eventId + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId ) fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 @@ -84,4 +85,7 @@ data class RoomDetailViewState( fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() fun isDm() = asyncRoomSummary()?.isDirect == true + + fun isThreadTimeline() = rootThreadEventId != null + } 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/TimelineFragment.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 6dfe29cec6..9afd5a2fc9 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/TimelineFragment.kt @@ -25,7 +25,6 @@ import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Parcelable import android.text.Spannable import android.text.format.DateUtils import android.view.HapticFeedbackConstants @@ -49,7 +48,6 @@ 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 @@ -134,6 +132,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView @@ -162,8 +161,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet -import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs -import im.vector.app.features.home.room.threads.detail.RoomThreadDetailActivity +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor @@ -188,7 +186,6 @@ import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog @@ -224,16 +221,7 @@ import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject -@Parcelize -data class RoomDetailArgs( - val roomId: String, - val eventId: String? = null, - val sharedData: SharedData? = null, - val openShareSpaceForId: String? = null, - val roomThreadDetailArgs: RoomThreadDetailArgs? = null -) : Parcelable - -class RoomDetailFragment @Inject constructor( +class TimelineFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, @@ -282,16 +270,16 @@ class RoomDetailFragment @Inject constructor( private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) - private val roomDetailArgs: RoomDetailArgs by args() + private val timelineArgs: TimelineArgs by args() private val glideRequests by lazy { GlideApp.with(this) } private val pillsPostProcessor by lazy { - pillsPostProcessorFactory.create(roomDetailArgs.roomId) + pillsPostProcessorFactory.create(timelineArgs.roomId) } private val autoCompleter: AutoCompleter by lazy { - autoCompleterFactory.create(roomDetailArgs.roomId, isThreadTimeLine()) + autoCompleterFactory.create(timelineArgs.roomId, isThreadTimeLine()) } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() @@ -308,6 +296,8 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel + private lateinit var sharedActivityActionViewModel: RoomDetailSharedActionViewModel + private lateinit var knownCallsViewModel: SharedKnownCallsViewModel private lateinit var layoutManager: LinearLayoutManager @@ -341,10 +331,11 @@ class RoomDetailFragment @Inject constructor( lifecycle.addObserver(ConferenceEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) + sharedActivityActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() callActionsHandler = StartCallActionsHandler( - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, fragment = this, vectorPreferences = vectorPreferences, roomDetailViewModel = roomDetailViewModel, @@ -355,11 +346,7 @@ class RoomDetailFragment @Inject constructor( ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) - if (isThreadTimeLine()) { - views.roomToolbar.isGone = true - } else { - setupToolbar(views.roomToolbar) - } + setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() setupNotificationView() @@ -370,8 +357,8 @@ class RoomDetailFragment @Inject constructor( setupRemoveJitsiWidgetView() setupVoiceMessageView() - views.roomToolbarContentView.debouncedClicks { - navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) + views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { + navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) } sharedActionViewModel @@ -456,7 +443,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) - RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), timelineArgs.roomId) RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> @@ -532,7 +519,7 @@ class RoomDetailFragment @Inject constructor( private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) { val tag = MigrateRoomBottomSheet::javaClass.name - MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) + MigrateRoomBottomSheet.newInstance(timelineArgs.roomId, roomDetailViewEvents.newVersion) .show(parentFragmentManager, tag) } @@ -578,7 +565,7 @@ class RoomDetailFragment @Inject constructor( private fun handleOpenRoomSettings() { navigator.openRoomProfile( requireContext(), - roomDetailArgs.roomId, + timelineArgs.roomId, RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS ) } @@ -600,7 +587,7 @@ class RoomDetailFragment @Inject constructor( WidgetArgs( baseUrl = it.domain, kind = WidgetKind.ROOM, - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, widgetId = it.widget.widgetId ) ).apply { @@ -626,7 +613,7 @@ class RoomDetailFragment @Inject constructor( navigator.openIntegrationManager( context = requireContext(), activityResultLauncher = integrationManagerActivityResultLauncher, - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, integId = null, screen = screen ) @@ -717,11 +704,11 @@ class RoomDetailFragment @Inject constructor( } private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { - navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) + navigator.openRoomWidget(requireContext(), timelineArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) } private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) { - navigator.openStickerPicker(requireContext(), stickerActivityResultLauncher, roomDetailArgs.roomId, event.widget) + navigator.openStickerPicker(requireContext(), stickerActivityResultLauncher, timelineArgs.roomId, event.widget) } private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) { @@ -795,7 +782,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleShareData() { - when (val sharedData = roomDetailArgs.sharedData) { + when (val sharedData = timelineArgs.sharedData) { is SharedData.Text -> { textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true)) } @@ -808,7 +795,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleSpaceShare() { - roomDetailArgs.openShareSpaceForId?.let { spaceId -> + timelineArgs.openShareSpaceForId?.let { spaceId -> ShareSpaceBottomSheet.show(childFragmentManager, spaceId, true) view?.post { handleChatEffect(ChatEffect.CONFETTI) @@ -909,7 +896,6 @@ 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 { @@ -923,15 +909,11 @@ 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) } + withState(roomDetailViewModel) { state -> // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined @@ -968,41 +950,72 @@ class RoomDetailFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.invite -> { - navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) + R.id.invite -> { + navigator.openInviteUsersToRoom(requireActivity(), timelineArgs.roomId) true } - R.id.timeline_setting -> { - navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) + R.id.timeline_setting -> { + navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) true } - R.id.open_matrix_apps -> { + R.id.open_matrix_apps -> { roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) true } - R.id.voice_call -> { + R.id.voice_call -> { callActionsHandler.onVoiceCallClicked() true } - R.id.video_call -> { + R.id.video_call -> { callActionsHandler.onVideoCallClicked() true } - R.id.search -> { + R.id.threads -> { + requireActivity().toast("View All Threads") + true + } + R.id.search -> { handleSearchAction() true } - R.id.dev_tools -> { - navigator.openDevTools(requireContext(), roomDetailArgs.roomId) + R.id.dev_tools -> { + navigator.openDevTools(requireContext(), timelineArgs.roomId) true } - else -> super.onOptionsItemSelected(item) + R.id.menu_thread_timeline_copy_link -> { + requireActivity().toast("menu_thread_timeline_copy_link") + true + } + R.id.menu_thread_timeline_view_in_room -> { + handleViewInRoomAction() + true + } + R.id.menu_thread_timeline_share -> { + requireActivity().toast("menu_thread_timeline_share") + true + } + else -> super.onOptionsItemSelected(item) + } + } + + /** + * View and highlight the original root thread message in the main timeline + */ + private fun handleViewInRoomAction(){ + getRootThreadEventId()?.let { + val newRoom = timelineArgs.copy(threadTimelineArgs = null,eventId = it) + context?.let{ con -> + val int = RoomDetailActivity.newIntent(con, newRoom) + int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + con.startActivity(int) + + } } } private fun handleSearchAction() { - if (session.getRoom(roomDetailArgs.roomId)?.isEncrypted() == false) { - navigator.openSearch(requireContext(), roomDetailArgs.roomId) + if (session.getRoom(timelineArgs.roomId)?.isEncrypted() == false) { + navigator.openSearch(requireContext(), timelineArgs.roomId) } else { showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room)) } @@ -1080,7 +1093,7 @@ class RoomDetailFragment @Inject constructor( override fun onResume() { super.onResume() - notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) + notificationDrawerManager.setCurrentRoom(timelineArgs.roomId) roomDetailPendingActionStore.data?.let { handlePendingAction(it) } roomDetailPendingActionStore.data = null @@ -1322,7 +1335,7 @@ class RoomDetailFragment @Inject constructor( views.composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { - attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment) + attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing) } @@ -1420,7 +1433,7 @@ class RoomDetailFragment @Inject constructor( } else if (summary?.membership == Membership.INVITE && inviter != null) { views.hideComposerViews() lazyLoadedViews.inviteView(true)?.apply { - callback = this@RoomDetailFragment + callback = this@TimelineFragment isVisible = true render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState) setOnClickListener { } @@ -1437,23 +1450,35 @@ class RoomDetailFragment @Inject constructor( } private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) { - if (roomSummary == null) { - views.roomToolbarContentView.isClickable = false + if (!isThreadTimeLine()) { + views.includeRoomToolbar.roomToolbarContentView.isVisible = true + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false + if (roomSummary == null) { + views.includeRoomToolbar.roomToolbarContentView.isClickable = false + } else { + views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN + views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName + avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) + renderSubTitle(typingMessage, roomSummary.topic) + views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) + views.includeRoomToolbar.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) + views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + } } else { - views.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN - views.roomToolbarTitleView.text = roomSummary.displayName - avatarRenderer.render(roomSummary.toMatrixItem(), views.roomToolbarAvatarImageView) - renderSubTitle(typingMessage, roomSummary.topic) - views.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) - views.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) - views.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + views.includeRoomToolbar.roomToolbarContentView.isVisible = false + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true + timelineArgs.threadTimelineArgs?.let { + val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) + views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName + } } } private fun renderSubTitle(typingMessage: String?, topic: String) { // TODO Temporary place to put typing data val subtitle = typingMessage?.takeIf { it.isNotBlank() } ?: topic - views.roomToolbarSubtitleView.apply { + views.includeRoomToolbar.roomToolbarSubtitleView.apply { setTextOrHide(subtitle) if (typingMessage.isNullOrBlank()) { setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) @@ -1592,14 +1617,14 @@ class RoomDetailFragment @Inject constructor( is RoomDetailAction.RequestVerification -> { Timber.v("## SAS RequestVerification action") VerificationBottomSheet.withArgs( - roomDetailArgs.roomId, + timelineArgs.roomId, data.userId ).show(parentFragmentManager, "REQ") } is RoomDetailAction.AcceptVerificationRequest -> { Timber.v("## SAS AcceptVerificationRequest action") VerificationBottomSheet.withArgs( - roomDetailArgs.roomId, + timelineArgs.roomId, data.otherUserId, data.transactionId ).show(parentFragmentManager, "REQ") @@ -1609,7 +1634,7 @@ class RoomDetailFragment @Inject constructor( VerificationBottomSheet().apply { arguments = Bundle().apply { putParcelable(Mavericks.KEY_ARG, VerificationBottomSheet.VerificationArgs( - otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) + otherUserId, data.transactionId, roomId = timelineArgs.roomId)) } }.show(parentFragmentManager, "REQ") } @@ -1624,7 +1649,7 @@ class RoomDetailFragment @Inject constructor( .launch(requireActivity(), url, object : NavigationInterceptor { override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { // Same room? - if (roomId == roomDetailArgs.roomId) { + if (roomId == timelineArgs.roomId) { // Navigation to same room if (eventId == null) { showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) @@ -1691,7 +1716,7 @@ class RoomDetailFragment @Inject constructor( override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { navigator.openMediaViewer( activity = requireActivity(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mediaData = mediaData, view = view ) { pairs -> @@ -1703,7 +1728,7 @@ class RoomDetailFragment @Inject constructor( override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { navigator.openMediaViewer( activity = requireActivity(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mediaData = mediaData, view = view ) { pairs -> @@ -1738,7 +1763,7 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) } - override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) { + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) { when (messageContent) { is MessageVerificationRequestContent -> { roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) @@ -1751,11 +1776,14 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } + if (isRootThreadEvent) { + navigateToThreadTimeline(informationData.eventId) + } } override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailArgs.roomId + val roomId = timelineArgs.roomId this.view?.hideKeyboard() MessageActionsBottomSheet @@ -1786,7 +1814,7 @@ class RoomDetailFragment @Inject constructor( } private fun openRoomMemberProfile(userId: String) { - navigator.openRoomMemberProfile(userId = userId, roomId = roomDetailArgs.roomId, context = requireActivity()) + navigator.openRoomMemberProfile(userId = userId, roomId = timelineArgs.roomId, context = requireActivity()) } override fun onMemberNameClicked(informationData: MessageInformationData) { @@ -1804,12 +1832,12 @@ class RoomDetailFragment @Inject constructor( } override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } override fun onEditedDecorationClicked(informationData: MessageInformationData) { - ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewEditHistoryBottomSheet.newInstance(timelineArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") } @@ -1921,7 +1949,7 @@ class RoomDetailFragment @Inject constructor( emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId)) } is EventSharedAction.ViewReactions -> { - ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } is EventSharedAction.Copy -> { @@ -1978,20 +2006,13 @@ class RoomDetailFragment @Inject constructor( } is EventSharedAction.ReplyInThread -> { if (!views.voiceMessageRecorderView.isActive()) { - context?.let { - val roomThreadDetailArgs = RoomThreadDetailArgs( - roomId = roomDetailArgs.roomId, - displayName = roomDetailViewModel.getRoomSummary()?.displayName, - avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl, - eventId = action.eventId) - startActivity(RoomThreadDetailActivity.newIntent(it, roomThreadDetailArgs)) - } + navigateToThreadTimeline(action.eventId) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } is EventSharedAction.CopyPermalink -> { - val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) + val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } @@ -2114,15 +2135,31 @@ class RoomDetailFragment @Inject constructor( .show() } + /** + * Navigate to Threads timeline for the specified threadRootEventId + * using the RoomThreadDetailActivity + */ + + private fun navigateToThreadTimeline(rootThreadEventId: String) { + context?.let { + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = timelineArgs.roomId, + displayName = roomDetailViewModel.getRoomSummary()?.displayName, + avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl, + rootThreadEventId = rootThreadEventId) + navigator.openThread(it, roomThreadDetailArgs) + } + } + // VectorInviteView.Callback override fun onAcceptInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.clearMemberShipNotificationForRoom(timelineArgs.roomId) roomDetailViewModel.handle(RoomDetailAction.AcceptInvite) } override fun onRejectInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.clearMemberShipNotificationForRoom(timelineArgs.roomId) roomDetailViewModel.handle(RoomDetailAction.RejectInvite) } @@ -2221,6 +2258,13 @@ class RoomDetailFragment @Inject constructor( } } - private fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null - fun getRootThreadEventId(): String? = roomDetailArgs.roomThreadDetailArgs?.eventId + /** + * Returns true if the current room is a Thread room, false otherwise + */ + private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null + + /** + * Returns the root thread event if we are in a thread room, otherwise returns null + */ + fun getRootThreadEventId(): String? = timelineArgs.threadTimelineArgs?.rootThreadEventId } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt new file mode 100644 index 0000000000..26455e04c7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt @@ -0,0 +1,31 @@ +/* + * 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 im.vector.app.features.home.room.detail.arguments + +import android.os.Parcelable +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs +import im.vector.app.features.share.SharedData +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TimelineArgs( + val roomId: String, + val eventId: String? = null, + val sharedData: SharedData? = null, + val openShareSpaceForId: String? = null, + val threadTimelineArgs: ThreadTimelineArgs? = null +) : Parcelable 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 bcc26247a2..af870c3313 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 @@ -29,7 +29,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand import im.vector.app.features.home.room.detail.ChatEffect -import im.vector.app.features.home.room.detail.RoomDetailFragment +import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsFlowFactory @@ -42,7 +42,6 @@ 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 @@ -772,7 +771,7 @@ class TextComposerViewModel @AssistedInject constructor( @JvmStatic override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel { - val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: TimelineFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.textComposerViewModelFactory.create(state) } } 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 f4dd5adebe..0e8d9e1e86 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 @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MavericksState -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -53,9 +53,9 @@ data class TextComposerViewState( val isComposerVisible: Boolean get() = canSendMessage && !isVoiceRecording - constructor(args: RoomDetailArgs) : this( + constructor(args: TimelineArgs) : this( roomId = args.roomId, - rootThreadEventId = args.roomThreadDetailArgs?.eventId) + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index d08259d739..11c90b3482 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -140,7 +140,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface BaseCallback { - fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) + fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean } 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 11061cbc9a..a30a0b851e 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 @@ -43,7 +43,7 @@ class MessageItemAttributesFactory @Inject constructor( callback?.onEventLongClicked(informationData, messageContent, view) ?: false }, itemClickListener = { view -> - callback?.onEventCellClicked(informationData, messageContent, view) + callback?.onEventCellClicked(informationData, messageContent, view, threadDetails?.isRootThread ?: false) }, memberClickListener = { callback?.onMemberNameClicked(informationData) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt deleted file mode 100644 index 0ad1d02ffb..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 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 im.vector.app.features.home.room.threads - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.widget.SearchView -import im.vector.app.R -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.ActivityFilteredRoomsBinding -import im.vector.app.databinding.ActivityRoomThreadsBinding -import im.vector.app.features.home.RoomListDisplayMode -import im.vector.app.features.home.room.list.RoomListFragment -import im.vector.app.features.home.room.list.RoomListParams - -class RoomThreadsActivity : VectorBaseActivity() { - -// private val roomListFragment: RoomListFragment? -// get() { -// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment -// } - - override fun getBinding() = ActivityRoomThreadsBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getMenuRes() = R.menu.menu_room_threads - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - configureToolbar(views.roomThreadsToolbar) -// if (isFirstCreation()) { -// val params = RoomListParams(RoomListDisplayMode.FILTERED) -// replaceFragment(R.id.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) -// } - } - - companion object { - private const val FRAGMENT_TAG = "RoomListFragment" - - fun newIntent(context: Context): Intent { - return Intent(context, RoomThreadsActivity::class.java) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt new file mode 100644 index 0000000000..73da1354af --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -0,0 +1,132 @@ +/* + * 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 im.vector.app.features.home.room.threads + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.google.android.material.appbar.MaterialToolbar +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.detail.TimelineFragment +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs +import im.vector.app.features.home.room.threads.detail.ThreadListFragment +import javax.inject.Inject + +class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable { + + @Inject + lateinit var avatarRenderer: AvatarRenderer + +// private val roomThreadDetailFragment: RoomThreadDetailFragment? +// get() { +// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment +// } + + override fun getBinding() = ActivityThreadsBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initFragment() + } + + private fun initFragment() { + if (isFirstCreation()) { + when (val fragment = fragmentToNavigate()) { + is DisplayFragment.ThreadList -> { + initThreadListFragment(fragment.threadListArgs) + } + is DisplayFragment.ThreadTimeLine -> { + initThreadTimelineFragment(fragment.threadTimelineArgs) + } + is DisplayFragment.ErrorFragment -> { + finish() + } + } + } + } + + private fun initThreadListFragment(threadListArgs: ThreadListArgs) { + replaceFragment( + R.id.threadsActivityFragmentContainer, + ThreadListFragment::class.java, + threadListArgs) + } + + private fun initThreadTimelineFragment(threadTimelineArgs: ThreadTimelineArgs) = + replaceFragment( + R.id.threadsActivityFragmentContainer, + TimelineFragment::class.java, + TimelineArgs( + roomId = threadTimelineArgs.roomId, + threadTimelineArgs = threadTimelineArgs + )) + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } + + /** + * Determine in witch fragment we should navigate + */ + private fun fragmentToNavigate(): DisplayFragment { + getThreadTimelineArgs()?.let { + return DisplayFragment.ThreadTimeLine(it) + } + getThreadListArgs()?.let { + return DisplayFragment.ThreadList(it) + } + return DisplayFragment.ErrorFragment + } + + private fun getThreadTimelineArgs(): ThreadTimelineArgs? = intent?.extras?.getParcelable(THREAD_TIMELINE_ARGS) + private fun getThreadListArgs(): ThreadListArgs? = intent?.extras?.getParcelable(THREAD_LIST_ARGS) + + companion object { + // private val FRAGMENT_TAG = RoomThreadDetailFragment::class.simpleName + const val THREAD_TIMELINE_ARGS = "THREAD_TIMELINE_ARGS" + const val THREAD_LIST_ARGS = "THREAD_LIST_ARGS" + + fun newIntent(context: Context, threadTimelineArgs: ThreadTimelineArgs?, threadListArgs: ThreadListArgs?): Intent { + return Intent(context, ThreadsActivity::class.java).apply { + putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) + putExtra(THREAD_LIST_ARGS, threadListArgs) + + } + } + } + + sealed class DisplayFragment { + data class ThreadList(val threadListArgs: ThreadListArgs) : DisplayFragment() + data class ThreadTimeLine(val threadTimelineArgs: ThreadTimelineArgs) : DisplayFragment() + object ErrorFragment : DisplayFragment() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt new file mode 100644 index 0000000000..23b72e5f32 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt @@ -0,0 +1,25 @@ +/* + * 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 im.vector.app.features.home.room.threads.arguments + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ThreadListArgs( + val roomId: String +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt similarity index 85% rename from vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt index 5fce24cd0d..2ebed2f745 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package im.vector.app.features.home.room.threads.detail.arguments +package im.vector.app.features.home.room.threads.arguments import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class RoomThreadDetailArgs( +data class ThreadTimelineArgs( val roomId: String, val displayName: String?, val avatarUrl: String?, - val eventId: String? = null, + val rootThreadEventId: String? = null ) : Parcelable 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 deleted file mode 100644 index c82fa353e4..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 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 im.vector.app.features.home.room.threads.detail - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import im.vector.app.R -import im.vector.app.core.di.ScreenComponent -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 - -class RoomThreadDetailActivity : VectorBaseActivity() { - - @Inject - lateinit var avatarRenderer: AvatarRenderer - -// private val roomThreadDetailFragment: RoomThreadDetailFragment? -// get() { -// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment -// } - - override fun getBinding() = ActivityRoomThreadDetailBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getMenuRes() = R.menu.menu_room_threads - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initToolbar() - initFragment() - } - - private fun initToolbar() { - configureToolbar(views.roomThreadDetailToolbar) - getRoomThreadDetailArgs()?.let { - val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) - avatarRenderer.render(matrixItem, views.roomThreadDetailToolbarImageView) - } - } - - private fun initFragment() { - if (isFirstCreation()) { - getRoomThreadDetailArgs()?.let { - replaceFragment( - R.id.roomThreadDetailFragmentContainer, - RoomDetailFragment::class.java, - RoomDetailArgs( - roomId = it.roomId, - roomThreadDetailArgs = it - )) - } -// replaceFragment(R.id.roomThreadDetailFragmentContainer, RoomThreadDetailFragment::class.java, getRoomThreadDetailArgs(), FRAGMENT_TAG) - } - } - - private fun getRoomThreadDetailArgs(): RoomThreadDetailArgs? = intent?.extras?.getParcelable(ROOM_THREAD_DETAIL_ARGS) - - companion object { - private val FRAGMENT_TAG = RoomThreadDetailFragment::class.simpleName - const val ROOM_THREAD_DETAIL_ARGS = "ROOM_THREAD_DETAIL_ARGS" - - fun newIntent(context: Context, roomThreadDetailArgs: RoomThreadDetailArgs): Intent { - return Intent(context, RoomThreadDetailActivity::class.java).apply { - putExtra(ROOM_THREAD_DETAIL_ARGS, roomThreadDetailArgs) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt similarity index 74% rename from vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt index fee21128cd..4e870bd53b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt @@ -16,34 +16,31 @@ package im.vector.app.features.home.room.threads.detail -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentRoomThreadDetailBinding -import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs +import im.vector.app.databinding.FragmentThreadListBinding +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class RoomThreadDetailFragment @Inject constructor( +class ThreadListFragment @Inject constructor( private val session: Session -) : VectorBaseFragment() { +) : VectorBaseFragment() { - private val roomThreadDetailArgs: RoomThreadDetailArgs by args() + private val threadTimelineArgs: ThreadTimelineArgs by args() - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomThreadDetailBinding { - return FragmentRoomThreadDetailBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding { + return FragmentThreadListBinding.inflate(inflater, container, false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } - @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initTextComposer() @@ -59,7 +56,7 @@ class RoomThreadDetailFragment @Inject constructor( } private fun initTextComposer(){ - views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true +// views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index debdf3739c..fdf1a24261 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -49,10 +49,13 @@ import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity +import im.vector.app.features.home.room.threads.ThreadsActivity +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig @@ -118,7 +121,7 @@ class DefaultNavigator @Inject constructor( fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast()) return } - val args = RoomDetailArgs(roomId, eventId) + val args = TimelineArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) startActivity(context, intent, buildTask) } @@ -141,7 +144,7 @@ class DefaultNavigator @Inject constructor( startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false) } is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { - val args = RoomDetailArgs( + val args = TimelineArgs( postSwitchSpaceAction.roomId, eventId = null, openShareSpaceForId = spaceId.takeIf { postSwitchSpaceAction.showShareSheet } @@ -239,7 +242,7 @@ class DefaultNavigator @Inject constructor( } override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) { - val args = RoomDetailArgs(roomId, null, sharedData) + val args = TimelineArgs(roomId, null, sharedData) val intent = RoomDetailActivity.newIntent(activity, args) activity.startActivity(intent) activity.finish() @@ -507,4 +510,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } } + + override fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) { + context.startActivity(ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = threadTimelineArgs, + threadListArgs =null)) + } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 612643c804..eeeb2a1b35 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -24,6 +24,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode @@ -140,4 +141,7 @@ interface Navigator { fun openDevTools(context: Context, roomId: String) fun openCallTransfer(context: Context, callId: String) + + fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) + } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 92feb3d038..7f41049c21 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -56,7 +56,7 @@ import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.themes.ThemeUtils @@ -497,7 +497,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) - .addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId))) + .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId))) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(contentPendingIntent) @@ -735,7 +735,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } private fun buildOpenRoomIntent(roomId: String): PendingIntent? { - val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) + val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId)) roomIntentTap.action = TAP_TO_VIEW_ACTION // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId") diff --git a/vector/src/main/res/drawable/ic_thread_link_menu_item.xml b/vector/src/main/res/drawable/ic_thread_link_menu_item.xml new file mode 100644 index 0000000000..779c9d832c --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_link_menu_item.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_thread_menu_item.xml b/vector/src/main/res/drawable/ic_thread_menu_item.xml new file mode 100644 index 0000000000..2d77251c53 --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_menu_item.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_thread_share_menu_item.xml b/vector/src/main/res/drawable/ic_thread_share_menu_item.xml new file mode 100644 index 0000000000..cb863c39bf --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_share_menu_item.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml b/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml new file mode 100644 index 0000000000..f408f99713 --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/vector/src/main/res/layout/activity_room_thread_detail.xml b/vector/src/main/res/layout/activity_room_thread_detail.xml deleted file mode 100644 index 94c52ab959..0000000000 --- a/vector/src/main/res/layout/activity_room_thread_detail.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_room_threads.xml b/vector/src/main/res/layout/activity_room_threads.xml deleted file mode 100644 index b469c7de42..0000000000 --- a/vector/src/main/res/layout/activity_room_threads.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_threads.xml b/vector/src/main/res/layout/activity_threads.xml new file mode 100644 index 0000000000..c34be9687d --- /dev/null +++ b/vector/src/main/res/layout/activity_threads.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 7725cd5e92..5d0e5f3ebd 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -26,101 +26,13 @@ android:layout_height="?actionBarSize" android:transitionName="toolbar"> - + - - - - - - - - - - - - - + diff --git a/vector/src/main/res/layout/fragment_room_thread_detail.xml b/vector/src/main/res/layout/fragment_room_thread_detail.xml deleted file mode 100644 index cadc819d28..0000000000 --- a/vector/src/main/res/layout/fragment_room_thread_detail.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml new file mode 100644 index 0000000000..dcb60a44d7 --- /dev/null +++ b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_room_detail_toolbar.xml b/vector/src/main/res/layout/view_room_detail_toolbar.xml new file mode 100644 index 0000000000..fdc3f6819e --- /dev/null +++ b/vector/src/main/res/layout/view_room_detail_toolbar.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_thread_timeline.xml b/vector/src/main/res/menu/menu_thread_timeline.xml new file mode 100644 index 0000000000..4698559bae --- /dev/null +++ b/vector/src/main/res/menu/menu_thread_timeline.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 54967f1706..532b63dd38 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -37,11 +37,20 @@ app:showAsAction="always" tools:visible="true" /> - + + + app:showAsAction="always" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b781692733..a85bf0629e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -441,6 +441,7 @@ Are you sure you want to sign out? Voice Call Video Call + View Threads Global search Mark all as read Historical @@ -456,6 +457,11 @@ Disable Return + + View in room + Copy link to thread + Share + Confirmation Warning @@ -1023,6 +1029,7 @@ Filter Threads in room + Thread Reason for reporting this content From 722f367690b0d7268397efaf6f4c05cfbe97180b Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 23 Nov 2021 13:34:24 +0200 Subject: [PATCH 011/130] View all threads screen implementation & UI Add user friendly message thread summary on the SDK side Fix not encrypted rooms thread summaries --- .../org/matrix/android/sdk/flow/FlowRoom.kt | 8 ++ .../sdk/api/session/events/model/Event.kt | 39 ++++++-- .../session/room/timeline/TimelineService.kt | 13 +++ .../database/helper/ThreadEventsHelper.kt | 13 ++- .../internal/database/mapper/EventMapper.kt | 4 +- .../session/room/timeline/DefaultTimeline.kt | 6 +- .../room/timeline/DefaultTimelineService.kt | 15 +++ .../debug/res/layout/fragment_thread_list.xml | 14 --- .../im/vector/app/core/di/FragmentModule.kt | 2 +- .../home/room/detail/TimelineFragment.kt | 24 ++++- .../detail/timeline/item/AbsMessageItem.kt | 18 ++-- .../home/room/threads/ThreadsActivity.kt | 3 +- .../room/threads/arguments/ThreadListArgs.kt | 4 +- .../room/threads/detail/ThreadListFragment.kt | 62 ------------- .../threads/list/model/ThreadSummaryModel.kt | 66 ++++++++++++++ .../list/viewmodel/ThreadSummaryController.kt | 73 +++++++++++++++ .../list/viewmodel/ThreadSummaryViewModel.kt | 72 +++++++++++++++ .../list/viewmodel/ThreadSummaryViewState.kt | 31 +++++++ .../threads/list/views/ThreadListFragment.kt | 91 +++++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 10 ++ .../app/features/navigation/Navigator.kt | 1 + .../main/res/layout/fragment_thread_list.xml | 40 ++++++++ .../main/res/layout/item_thread_summary.xml | 91 +++++++++++++++++++ .../res/layout/item_timeline_event_base.xml | 24 ++++- .../res/layout/view_thread_room_summary.xml | 58 +++++------- .../src/main/res/menu/menu_room_threads.xml | 9 -- vector/src/main/res/menu/menu_thread_list.xml | 13 +++ .../main/res/menu/menu_thread_timeline.xml | 36 -------- 28 files changed, 654 insertions(+), 186 deletions(-) delete mode 100644 vector/src/debug/res/layout/fragment_thread_list.xml delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt create mode 100644 vector/src/main/res/layout/fragment_thread_list.xml create mode 100644 vector/src/main/res/layout/item_thread_summary.xml delete mode 100644 vector/src/main/res/menu/menu_room_threads.xml create mode 100644 vector/src/main/res/menu/menu_thread_list.xml delete mode 100644 vector/src/main/res/menu/menu_thread_timeline.xml diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 42c1476b79..7091905991 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt @@ -98,6 +99,13 @@ class FlowRoom(private val room: Room) { fun liveNotificationState(): Flow { return room.getLiveRoomNotificationState().asFlow() } + + fun liveThreadList(): Flow> { + return room.getAllThreadsLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getAllThreads() + } + } } fun Room.flow(): FlowRoom { 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 ccf98f7754..77285dd463 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 @@ -27,11 +27,13 @@ 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.threads.ThreadDetails +import org.matrix.android.sdk.api.util.ContentUtils import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.session.presence.model.PresenceContent +import org.matrix.android.sdk.internal.session.room.send.removeInReplyFallbacks import timber.log.Timber typealias Content = JsonDict @@ -188,14 +190,39 @@ data class Event( return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } } - fun getDecryptedMessageText(): String { - return getValueFromPayload(mxDecryptionResult?.payload).orEmpty() + /** + * Returns a user friendly content depending on the message type. + * It can be used especially for message summaries. + * It will return a decrypted text message or an empty string otherwise. + */ + fun getDecryptedUserFriendlyTextSummary(): String { + val text = getDecryptedValue().orEmpty() + return when { + isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) + isFileMessage() -> "sent a file." + isAudioMessage() -> "sent an audio file." + isImageMessage() -> "sent an image." + isVideoMessage() -> "sent a video." + else -> text + } } - @Suppress("UNCHECKED_CAST") - private fun getValueFromPayload(payload: JsonDict?, key: String = "body"): String? { - val content = payload?.get("content") as? JsonDict - return content?.get(key) as? String + private fun Event.isQuote(): Boolean { + if (isReply()) return false + return getDecryptedValue("formatted_body")?.contains("
") ?: false + } + + /** + * Decrypt the message, or return the pure payload value if there is no encryption + */ + private fun getDecryptedValue(key: String = "body"): String? { + return if (isEncrypted()) { + @Suppress("UNCHECKED_CAST") + val content = mxDecryptionResult?.payload?.get("content") as? JsonDict + content?.get(key) as? String + } else { + content?.get(key) as? String + } } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 3c021384e1..aa70343279 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -55,4 +55,17 @@ interface TimelineService { * Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO. */ fun getAttachmentMessages(): List + + /** + * Get a live list of all the thread for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getAllThreadsLive(): LiveData> + + /** + * Get a list of all the thread for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getAllThreads(): List + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 597e08e307..755891af3e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm +import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import org.matrix.android.sdk.BuildConfig @@ -82,7 +83,17 @@ internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEv TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() +/** + * Find all TimelineEventEntity that are root threads for the specified room + * @param roomId The room that all stored root threads will be returned + */ +internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD,true) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) 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 de4be16493..aded11e815 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 @@ -22,11 +22,9 @@ 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.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.threads.ThreadDetails -import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.di.MoshiProvider @@ -113,7 +111,7 @@ internal object EventMapper { avatarUrl = timelineEventEntity.senderAvatar ) }, - threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedMessageText().orEmpty() + threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedUserFriendlyTextSummary().orEmpty() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index a8a72d8a52..4d417fddbb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -168,7 +168,11 @@ internal class DefaultTimeline( TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it) - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + .or() + .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + .findAll() + } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() timelineEvents.addChangeListener(eventsChangeListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 47e8f7e3a3..690f300827 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -31,9 +31,11 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask @@ -102,4 +104,17 @@ internal class DefaultTimelineService @AssistedInject constructor( .orEmpty() } } + + override fun getAllThreadsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + override fun getAllThreads(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } } diff --git a/vector/src/debug/res/layout/fragment_thread_list.xml b/vector/src/debug/res/layout/fragment_thread_list.xml deleted file mode 100644 index cf3a79e776..0000000000 --- a/vector/src/debug/res/layout/fragment_thread_list.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 4763e6f935..37418f4c63 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -58,7 +58,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.list.RoomListFragment -import im.vector.app.features.home.room.threads.detail.ThreadListFragment +import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 9afd5a2fc9..a8e8e11b57 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -68,6 +68,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges import com.vanniktech.emoji.EmojiPopup +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper @@ -971,7 +972,7 @@ class TimelineFragment @Inject constructor( true } R.id.threads -> { - requireActivity().toast("View All Threads") + navigateToThreadList() true } R.id.search -> { @@ -1776,7 +1777,7 @@ class TimelineFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } - if (isRootThreadEvent) { + if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) { navigateToThreadTimeline(informationData.eventId) } } @@ -2136,8 +2137,8 @@ class TimelineFragment @Inject constructor( } /** - * Navigate to Threads timeline for the specified threadRootEventId - * using the RoomThreadDetailActivity + * Navigate to Threads timeline for the specified rootThreadEventId + * using the ThreadsActivity */ private fun navigateToThreadTimeline(rootThreadEventId: String) { @@ -2151,6 +2152,21 @@ class TimelineFragment @Inject constructor( } } + /** + * Navigate to Threads list for the current room + * using the ThreadsActivity + */ + + private fun navigateToThreadList() { + context?.let { + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = timelineArgs.roomId, + displayName = roomDetailViewModel.getRoomSummary()?.displayName, + avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl) + navigator.openThreadList(it, roomThreadDetailArgs) + } + } + // VectorInviteView.Callback override fun onAcceptInvite() { 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 0649755c2a..188a195ae6 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 @@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick @@ -105,14 +106,17 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA // Threads - attributes.threadDetails?.let { threadDetails -> - threadDetails.isRootThread - holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread - holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() - holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage - threadDetails.threadSummarySenderInfo?.let { senderInfo -> - attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) + if(BuildConfig.THREADING_ENABLED) { + attributes.threadDetails?.let { threadDetails -> + holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread + holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() + holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage + threadDetails.threadSummarySenderInfo?.let { senderInfo -> + attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) + } } + }else{ + holder.threadSummaryConstraintLayout.isVisible = false } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index 73da1354af..007b419532 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -25,14 +25,13 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.databinding.ActivityThreadsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs -import im.vector.app.features.home.room.threads.detail.ThreadListFragment +import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import javax.inject.Inject class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt index 23b72e5f32..50819a3017 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt @@ -21,5 +21,7 @@ import kotlinx.parcelize.Parcelize @Parcelize data class ThreadListArgs( - val roomId: String + val roomId: String, + val displayName: String?, + val avatarUrl: String?, ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt deleted file mode 100644 index 4e870bd53b..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 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 im.vector.app.features.home.room.threads.detail - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.airbnb.mvrx.args -import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentThreadListBinding -import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs -import org.matrix.android.sdk.api.session.Session -import javax.inject.Inject - -class ThreadListFragment @Inject constructor( - private val session: Session -) : VectorBaseFragment() { - - private val threadTimelineArgs: ThreadTimelineArgs by args() - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding { - return FragmentThreadListBinding.inflate(inflater, container, false) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initTextComposer() -// lifecycleScope.launch(Dispatchers.IO) { -// Realm.getInstance(realmConfiguration).executeTransaction { -// val eventId = roomThreadDetailArgs.eventId ?: return@executeTransaction -// val r = EventEntity.where(it, eventId = eventId) -// .findFirst() ?: return@executeTransaction -// Timber.i("------> $eventId isThread: ${EventMapper.map(r).isThread()}") -// } -// } -//// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}" - } - - private fun initTextComposer(){ -// views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true - } - -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt new file mode 100644 index 0000000000..85e375d00d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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 im.vector.app.features.home.room.threads.list.model + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_thread_summary) +abstract class ThreadSummaryModel : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute lateinit var title: String + @EpoxyAttribute lateinit var date: String + @EpoxyAttribute lateinit var rootMessage: String + @EpoxyAttribute lateinit var lastMessage: String + @EpoxyAttribute lateinit var lastMessageCounter: String + @EpoxyAttribute lateinit var lastMessageMatrixItem: MatrixItem + + override fun bind(holder: Holder) { + super.bind(holder) + avatarRenderer.render(matrixItem, holder.avatarImageView) + holder.avatarImageView.contentDescription = matrixItem.getBestName() + holder.titleTextView.text = title + holder.dateTextView.text = date + holder.rootMessageTextView.text = rootMessage + + // Last message summary + avatarRenderer.render(lastMessageMatrixItem, holder.lastMessageAvatarImageView) + holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem.getBestName() + holder.lastMessageTextView.text = lastMessage + holder.lastMessageCounterTextView.text = lastMessageCounter + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.threadSummaryAvatarImageView) + val titleTextView by bind(R.id.threadSummaryTitleTextView) + val dateTextView by bind(R.id.threadSummaryDateTextView) + val rootMessageTextView by bind(R.id.threadSummaryRootMessageTextView) + val lastMessageAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) + val lastMessageCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) + val lastMessageTextView by bind(R.id.messageThreadSummaryInfoTextView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt new file mode 100644 index 0000000000..bd19c8e3ff --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2020 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 im.vector.app.features.home.room.threads.list.viewmodel + +import com.airbnb.epoxy.EpoxyController +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.threads.list.model.threadSummary +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class ThreadSummaryController @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: ThreadSummaryViewState? = null + + init { + // We are requesting a model build directly as the first build of epoxy is on the main thread. + // It avoids to build the whole list of breadcrumbs on the main thread. + requestModelBuild() + } + + fun update(viewState: ThreadSummaryViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val safeViewState = viewState ?: return + val host = this + // Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client +// zeroItem { +// id("top") +// } + + // An empty breadcrumbs list can only be temporary because when entering in a room, + // this one is added to the breadcrumbs + safeViewState.rootThreadEventList.invoke() + ?.forEach { timelineEvent -> + threadSummary { + id(timelineEvent.eventId) + avatarRenderer(host.avatarRenderer) + matrixItem(timelineEvent.senderInfo.toMatrixItem()) + title(timelineEvent.senderInfo.displayName) + date(timelineEvent.root.ageLocalTs.toString()) + rootMessage(timelineEvent.root.getDecryptedUserFriendlyTextSummary()) + lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) + lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) + lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) + } + } + } + + interface Listener { + fun onBreadcrumbClicked(roomId: String) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt new file mode 100644 index 0000000000..385213470a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2020 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 im.vector.app.features.home.room.threads.list.viewmodel + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.flow.flow + +class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState, + private val session: Session) : + VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId) + + @AssistedFactory + interface Factory { + fun create(initialState: ThreadSummaryViewState): ThreadSummaryViewModel + } + + companion object : MavericksViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ThreadSummaryViewState): ThreadSummaryViewModel? { + val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.threadSummaryViewModelFactory.create(state) + } + } + + init { + observeThreadsSummary() + } + + override fun handle(action: EmptyAction) { + // No op + } + + + private fun observeThreadsSummary() { + room?.flow() + ?.liveThreadList() + ?.execute { asyncThreads -> + copy(rootThreadEventList = asyncThreads) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt new file mode 100644 index 0000000000..b0c9c2ea26 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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 im.vector.app.features.home.room.threads.list.viewmodel + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +data class ThreadSummaryViewState( + val rootThreadEventList: Async> = Uninitialized, + val roomId: String +) : MavericksState{ + + constructor(args: ThreadListArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt new file mode 100644 index 0000000000..d2551b58c1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -0,0 +1,91 @@ +/* + * 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 im.vector.app.features.home.room.threads.list.views + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentThreadListBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsAnimator +import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel +import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject + +class ThreadListFragment @Inject constructor( + private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val threadSummaryController: ThreadSummaryController, + val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory +) : VectorBaseFragment() { + + private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel() + + private val threadListArgs: ThreadListArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding { + return FragmentThreadListBinding.inflate(inflater, container, false) + } + + override fun getMenuRes() = R.menu.menu_thread_list + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initToolbar() + views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false) +// threadSummaryController.listener = this + } + + override fun onDestroyView() { + views.threadListRecyclerView.cleanup() +// breadcrumbsController.listener = null + super.onDestroyView() + } + private fun initToolbar(){ + setupToolbar(views.threadListToolbar) + renderToolbar() + } + + override fun invalidate() = withState(threadSummaryViewModel) { state -> + threadSummaryController.update(state) + } + + private fun renderToolbar() { + views.includeThreadListToolbar.roomToolbarThreadConstraintLayout.isVisible = true + val matrixItem = MatrixItem.RoomItem(threadListArgs.roomId, threadListArgs.displayName, threadListArgs.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadListToolbar.roomToolbarThreadImageView) + views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName + } +} diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index fdf1a24261..5f4a2168e9 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -517,4 +517,14 @@ class DefaultNavigator @Inject constructor( threadTimelineArgs = threadTimelineArgs, threadListArgs =null)) } + override fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) { + context.startActivity(ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = null, + threadListArgs = ThreadListArgs( + roomId = threadTimelineArgs.roomId, + displayName = threadTimelineArgs.displayName, + avatarUrl = threadTimelineArgs.avatarUrl + ))) + } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index eeeb2a1b35..02452cf6fc 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -143,5 +143,6 @@ interface Navigator { fun openCallTransfer(context: Context, callId: String) fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) + fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) } diff --git a/vector/src/main/res/layout/fragment_thread_list.xml b/vector/src/main/res/layout/fragment_thread_list.xml new file mode 100644 index 0000000000..25dd200737 --- /dev/null +++ b/vector/src/main/res/layout/fragment_thread_list.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_thread_summary.xml b/vector/src/main/res/layout/item_thread_summary.xml new file mode 100644 index 0000000000..075709ef00 --- /dev/null +++ b/vector/src/main/res/layout/item_thread_summary.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 a1e1827d52..7094a28daa 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -200,7 +200,27 @@ - + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_room_summary.xml b/vector/src/main/res/layout/view_thread_room_summary.xml index 31bdd5ce06..59e2952b46 100644 --- a/vector/src/main/res/layout/view_thread_room_summary.xml +++ b/vector/src/main/res/layout/view_thread_room_summary.xml @@ -1,74 +1,58 @@ - + xmlns:tools="http://schemas.android.com/tools" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + android:src="@drawable/ic_thread_summary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + tools:text="Hello There, whats up! Its a large centence, whats up! Its a large centence" /> + diff --git a/vector/src/main/res/menu/menu_room_threads.xml b/vector/src/main/res/menu/menu_room_threads.xml deleted file mode 100644 index 3d4478332a..0000000000 --- a/vector/src/main/res/menu/menu_room_threads.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_thread_list.xml b/vector/src/main/res/menu/menu_thread_list.xml new file mode 100644 index 0000000000..6da0f80112 --- /dev/null +++ b/vector/src/main/res/menu/menu_thread_list.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_thread_timeline.xml b/vector/src/main/res/menu/menu_thread_timeline.xml deleted file mode 100644 index 4698559bae..0000000000 --- a/vector/src/main/res/menu/menu_thread_timeline.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file From 5e5ce614ef6d2c0116cf9b69379852e0cbc6d0bf Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 23 Nov 2021 17:09:58 +0200 Subject: [PATCH 012/130] Add date in view all threads UI Improvements on threads summary Add View In Room bottom sheet action from within thread timeline root message --- .../home/room/detail/RoomDetailViewModel.kt | 4 +-- .../home/room/detail/TimelineFragment.kt | 7 ++++ .../timeline/TimelineEventController.kt | 10 +++--- .../timeline/action/EventSharedAction.kt | 6 +++- .../action/MessageActionsViewModel.kt | 35 +++++++++++++++++++ .../factory/MergedHeaderItemFactory.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 11 +++++- .../timeline/factory/TimelineItemFactory.kt | 10 +++--- .../factory/TimelineItemFactoryParams.kt | 2 ++ .../helper/TimelineEventVisibilityHelper.kt | 17 ++++----- .../detail/timeline/item/AbsMessageItem.kt | 4 +-- .../threads/list/model/ThreadSummaryModel.kt | 8 +++-- .../list/viewmodel/ThreadSummaryController.kt | 8 +++-- .../main/res/layout/item_thread_summary.xml | 12 +++---- .../res/layout/view_thread_room_summary.xml | 3 +- vector/src/main/res/values/strings.xml | 1 + 16 files changed, 103 insertions(+), 37 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 5854d35fb6..1f5550b27f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -676,7 +676,7 @@ class RoomDetailViewModel @AssistedInject constructor( if (initialState.isThreadTimeline()) { when (itemId) { R.id.menu_thread_timeline_more -> true - else -> false + else -> false } } else { when (itemId) { @@ -688,7 +688,7 @@ class RoomDetailViewModel @AssistedInject constructor( // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined R.id.search -> true - R.id.threads -> true + R.id.threads -> BuildConfig.THREADING_ENABLED R.id.dev_tools -> vectorPreferences.developerMode() else -> false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a8e8e11b57..30d4a881f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -2012,6 +2012,13 @@ class TimelineFragment @Inject constructor( requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } + is EventSharedAction.ViewInRoom -> { + if (!views.voiceMessageRecorderView.isActive()) { + handleViewInRoomAction() + } else { + requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + } + } is EventSharedAction.CopyPermalink -> { val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 11c90b3482..caa4783573 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -104,6 +104,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec roomSummary = state.asyncRoomSummary(), rootThreadEventId = state.rootThreadEventId ) + + fun isFromThreadTimeline():Boolean = rootThreadEventId != null } interface Callback : @@ -193,7 +195,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // it's sent by the same user so we are sure we have up to date information. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { - timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId ) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline() ) } if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { modelCache[prevDisplayableEventIndex] = null @@ -370,7 +372,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val nextEvent = currentSnapshot.nextOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position) val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { - timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId) + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline()) } // Should be build if not cached or if model should be refreshed if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { @@ -452,7 +454,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return null } // If the event is not shown, we go to the next one - if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) { + if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) { continue } // If the event is sent by us, we update the holder with the eventId and stop the search @@ -474,7 +476,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId } - if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) { + if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) { lastShownEventId = event.eventId } if (lastShownEventId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index c57d844974..7bffba69b4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -48,10 +48,14 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Reply(val eventId: String) : EventSharedAction(R.string.reply, R.drawable.ic_reply) + // TODO add translations data class ReplyInThread(val eventId: String) : - // TODO add translations EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) + // TODO add translations + object ViewInRoom : + EventSharedAction(R.string.view_in_room, R.drawable.ic_thread_view_in_room_menu_item) + data class Share(val eventId: String, val messageContent: MessageContent) : EventSharedAction(R.string.share, R.drawable.ic_share) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 6762ed1479..76e415eceb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -331,6 +331,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.ReplyInThread(eventId)) } + if (canViewInRoom(timelineEvent, messageContent, actionPermissions)) { + add(EventSharedAction.ViewInRoom) + } + if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { add(EventSharedAction.Edit(eventId)) } @@ -417,6 +421,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } + /** + * Determine whether or not the Reply In Thread bottom sheet setting will be visible + * to the user + */ + // TODO handle reply in thread for images etc private fun canReplyInThread(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { @@ -437,6 +446,32 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } + /** + * Determine whether or no the selected event is a root thread event from within + * a thread timeline + */ + private fun canViewInRoom(event: TimelineEvent, + messageContent: MessageContent?, + actionPermissions: ActionPermissions): Boolean { + // Only event of type EventType.MESSAGE are supported for the moment + if (!BuildConfig.THREADING_ENABLED) return false + if (!initialState.isFromThreadTimeline) return false + if (event.root.getClearType() != EventType.MESSAGE) return false + if (!actionPermissions.canSendMessage) return false + + return when (messageContent?.msgType) { + MessageType.MSGTYPE_TEXT -> event.root.threadDetails?.isRootThread ?: false +// MessageType.MSGTYPE_NOTICE, +// MessageType.MSGTYPE_EMOTE, +// MessageType.MSGTYPE_IMAGE, +// MessageType.MSGTYPE_VIDEO, +// MessageType.MSGTYPE_AUDIO, +// MessageType.MSGTYPE_FILE -> true + else -> false + } + } + + private fun canQuote(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { // Only event of type EventType.MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index fa699f0c78..1c25f923cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { - val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.rootThreadEventId) + val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.isFromThreadTimeline()) return if (mergedEvents.isEmpty()) { null } else { 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 0ee28404df..5e25b52473 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 @@ -125,6 +125,9 @@ class MessageItemFactory @Inject constructor( pillsPostProcessorFactory.create(roomId) } + + + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { val event = params.event val highlight = params.isHighlighted @@ -149,7 +152,10 @@ 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, event.root.threadDetails) + + // always hide summary when we are on thread timeline + val threadDetails = if(params.isFromThreadTimeline()) null else event.root.threadDetails + val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails) // val all = event.root.toContent() // val ev = all.toModel() @@ -174,6 +180,9 @@ class MessageItemFactory @Inject constructor( } } + private fun isFromThreadTimeline(params: TimelineItemFactoryParams){ + params.rootThreadEventId + } private fun buildOptionsMessageItem(messageContent: MessageOptionsContent, informationData: MessageInformationData, highlight: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 96786e3377..11c46026d6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -42,8 +42,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> { val event = params.event val computedModel = try { - if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.rootThreadEventId)) { - return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId) + if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.isFromThreadTimeline())) { + return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.isFromThreadTimeline()) } when (event.root.getClearType()) { // Message itemsX @@ -109,11 +109,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me Timber.e(throwable, "failed to create message item") defaultItemFactory.create(params, throwable) } - return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId) + return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.isFromThreadTimeline()) } - private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, rootThreadEventId: String?): TimelineEmptyItem { - val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, rootThreadEventId) + private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, isFromThreadTimeline: Boolean): TimelineEmptyItem { + val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, isFromThreadTimeline) return TimelineEmptyItem_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 94e94911c0..8479d6b589 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -38,4 +38,6 @@ data class TimelineItemFactoryParams( get() = partialState.rootThreadEventId val isHighlighted = highlightedEventId == event.eventId + + fun isFromThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index c56e9d1336..59a6c82aff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -40,7 +40,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * * @return a list of timeline events which have sequentially the same type following the next direction. */ - private fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List { + private fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List { if (index >= timelineEvents.size - 1) { return emptyList() } @@ -62,7 +62,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else { nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) } - val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, rootThreadEventId) } + val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, isFromThreadTimeline) } if (filteredSameTypeEvents.size < minSize) { return emptyList() } @@ -77,21 +77,22 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen * * @return a list of timeline events which have sequentially the same type following the prev direction. */ - fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List { + fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List { val prevSub = timelineEvents.subList(0, index + 1) return prevSub .reversed() .let { - nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId) + nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, isFromThreadTimeline) } } /** * @param timelineEvent the event to check for visibility * @param highlightedEventId can be checked to force visibility to true + * @param rootThreadEventId if this param is null it means we are in the original timeline * @return true if the event should be shown in the timeline. */ - fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, rootThreadEventId: String?): Boolean { + fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, isFromThreadTimeline: Boolean): Boolean { // If show hidden events is true we should always display something if (userPreferencesProvider.shouldShowHiddenEvents()) { return true @@ -105,14 +106,14 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences. - return !timelineEvent.shouldBeHidden(rootThreadEventId) + return !timelineEvent.shouldBeHidden(isFromThreadTimeline) } private fun TimelineEvent.isDisplayable(): Boolean { return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) } - private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?): Boolean { + private fun TimelineEvent.shouldBeHidden(isFromThreadTimeline: Boolean): Boolean { if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { return true } @@ -125,7 +126,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true } - if(BuildConfig.THREADING_ENABLED && rootThreadEventId == null && root.isThread() && root.getRootThreadEventId() != null){ + if(BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null){ return true } 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 188a195ae6..ba7865ac57 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 @@ -114,9 +114,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem threadDetails.threadSummarySenderInfo?.let { senderInfo -> attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) } - } - }else{ - holder.threadSummaryConstraintLayout.isVisible = false + } ?: run{holder.threadSummaryConstraintLayout.isVisible = false} } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt index 85e375d00d..57cba163eb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt @@ -37,7 +37,7 @@ abstract class ThreadSummaryModel : VectorEpoxyModel( @EpoxyAttribute lateinit var rootMessage: String @EpoxyAttribute lateinit var lastMessage: String @EpoxyAttribute lateinit var lastMessageCounter: String - @EpoxyAttribute lateinit var lastMessageMatrixItem: MatrixItem + @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null override fun bind(holder: Holder) { super.bind(holder) @@ -48,8 +48,10 @@ abstract class ThreadSummaryModel : VectorEpoxyModel( holder.rootMessageTextView.text = rootMessage // Last message summary - avatarRenderer.render(lastMessageMatrixItem, holder.lastMessageAvatarImageView) - holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem.getBestName() + lastMessageMatrixItem?.let { + avatarRenderer.render(it, holder.lastMessageAvatarImageView) + } + holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName() holder.lastMessageTextView.text = lastMessage holder.lastMessageCounterTextView.text = lastMessageCounter } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt index bd19c8e3ff..a47d2b6c3b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt @@ -17,13 +17,16 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.threads.list.model.threadSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class ThreadSummaryController @Inject constructor( - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val dateFormatter: VectorDateFormatter ) : EpoxyController() { var listener: Listener? = null @@ -53,12 +56,13 @@ class ThreadSummaryController @Inject constructor( // this one is added to the breadcrumbs safeViewState.rootThreadEventList.invoke() ?.forEach { timelineEvent -> + val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST) threadSummary { id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) matrixItem(timelineEvent.senderInfo.toMatrixItem()) title(timelineEvent.senderInfo.displayName) - date(timelineEvent.root.ageLocalTs.toString()) + date(date) rootMessage(timelineEvent.root.getDecryptedUserFriendlyTextSummary()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) diff --git a/vector/src/main/res/layout/item_thread_summary.xml b/vector/src/main/res/layout/item_thread_summary.xml index 075709ef00..130dae44b1 100644 --- a/vector/src/main/res/layout/item_thread_summary.xml +++ b/vector/src/main/res/layout/item_thread_summary.xml @@ -31,7 +31,7 @@ app:layout_constraintEnd_toStartOf="@id/threadSummaryDateTextView" app:layout_constraintStart_toEndOf="@id/threadSummaryAvatarImageView" app:layout_constraintTop_toTopOf="parent" - tools:text="Aris" /> + tools:text="Aris Kots" /> + tools:text="10 minutes" /> + tools:text="192" /> Edit Reply Reply In Thread + View In Room Retry "Join a room to start using the app." From e2bf3e7097317e54a61861bef53205ac6f92141b Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 23 Nov 2021 22:22:58 +0200 Subject: [PATCH 013/130] Add navigation to thread from the thread list Add thread creation from photos, files, images, audios, replies etc Add slide animation to view threads from thread list Add click listener on room summary to handle cases like ( there is an image and the user upon click should view the image instead of navigating to the thread, so he can click the thread summary ) --- .../home/room/detail/TimelineFragment.kt | 22 +++++++++---- .../timeline/TimelineEventController.kt | 5 +++ .../action/MessageActionsViewModel.kt | 28 ++++++++-------- .../helper/MessageItemAttributesFactory.kt | 1 + .../timeline/item/AbsBaseMessageItem.kt | 1 - .../detail/timeline/item/AbsMessageItem.kt | 8 +++++ .../room/detail/timeline/item/NoticeItem.kt | 3 +- .../home/room/threads/ThreadsActivity.kt | 33 +++++++++++++++++++ .../threads/list/model/ThreadSummaryModel.kt | 8 +++++ .../list/viewmodel/ThreadSummaryController.kt | 6 +++- .../threads/list/views/ThreadListFragment.kt | 26 +++++++++++---- .../app/features/navigation/Navigator.kt | 1 + .../main/res/anim/animation_slide_in_left.xml | 5 +++ .../res/anim/animation_slide_in_right.xml | 5 +++ .../res/anim/animation_slide_out_left.xml | 5 +++ .../res/anim/animation_slide_out_right.xml | 5 +++ .../main/res/layout/item_thread_summary.xml | 10 ++++-- .../view_room_detail_thread_toolbar.xml | 8 ++--- .../res/layout/view_thread_room_summary.xml | 8 ++--- vector/src/main/res/values/strings.xml | 3 +- 20 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 vector/src/main/res/anim/animation_slide_in_left.xml create mode 100644 vector/src/main/res/anim/animation_slide_in_right.xml create mode 100644 vector/src/main/res/anim/animation_slide_out_left.xml create mode 100644 vector/src/main/res/anim/animation_slide_out_right.xml diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 30d4a881f2..1c37b26c60 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1002,10 +1002,10 @@ class TimelineFragment @Inject constructor( /** * View and highlight the original root thread message in the main timeline */ - private fun handleViewInRoomAction(){ + private fun handleViewInRoomAction() { getRootThreadEventId()?.let { - val newRoom = timelineArgs.copy(threadTimelineArgs = null,eventId = it) - context?.let{ con -> + val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it) + context?.let { con -> val int = RoomDetailActivity.newIntent(con, newRoom) int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK con.startActivity(int) @@ -1473,6 +1473,7 @@ class TimelineFragment @Inject constructor( avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName } + views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } } @@ -1776,9 +1777,9 @@ class TimelineFragment @Inject constructor( is EncryptedEventContent -> { roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } - } - if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) { - navigateToThreadTimeline(informationData.eventId) + else -> { + onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) + } } } @@ -1809,6 +1810,13 @@ class TimelineFragment @Inject constructor( } } + override fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) { + if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) { + navigateToThreadTimeline(eventId) + } + } + + override fun onAvatarClicked(informationData: MessageInformationData) { // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId)) openRoomMemberProfile(informationData.senderId) @@ -2012,7 +2020,7 @@ class TimelineFragment @Inject constructor( requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } - is EventSharedAction.ViewInRoom -> { + is EventSharedAction.ViewInRoom -> { if (!views.voiceMessageRecorderView.isActive()) { handleViewInRoomAction() } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index caa4783573..b091ea2fb7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -112,6 +112,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec BaseCallback, ReactionPillCallback, AvatarCallback, + ThreadCallback, UrlClickCallback, ReadReceiptsCallback, PreviewUrlCallback { @@ -151,6 +152,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onMemberNameClicked(informationData: MessageInformationData) } + interface ThreadCallback { + fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) + } + interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) fun onReadMarkerVisible() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 76e415eceb..5f30ca6c39 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -435,13 +435,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted if (event.root.getClearType() != EventType.MESSAGE) return false if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { - MessageType.MSGTYPE_TEXT -> true -// MessageType.MSGTYPE_NOTICE, -// MessageType.MSGTYPE_EMOTE, -// MessageType.MSGTYPE_IMAGE, -// MessageType.MSGTYPE_VIDEO, -// MessageType.MSGTYPE_AUDIO, -// MessageType.MSGTYPE_FILE -> true + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> true else -> false } } @@ -460,13 +460,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { - MessageType.MSGTYPE_TEXT -> event.root.threadDetails?.isRootThread ?: false -// MessageType.MSGTYPE_NOTICE, -// MessageType.MSGTYPE_EMOTE, -// MessageType.MSGTYPE_IMAGE, -// MessageType.MSGTYPE_VIDEO, -// MessageType.MSGTYPE_AUDIO, -// MessageType.MSGTYPE_FILE -> true + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> event.root.threadDetails?.isRootThread ?: false else -> false } } 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 a30a0b851e..8cc5ffe1ee 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 @@ -50,6 +50,7 @@ class MessageItemAttributesFactory @Inject constructor( }, reactionPillCallback = callback, avatarCallback = callback, + threadCallback = callback, readReceiptsCallback = callback, emojiTypeFace = emojiCompatFontProvider.typeface, threadDetails = threadDetails diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 080b766258..a3e808c7bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -127,7 +127,6 @@ abstract class AbsBaseMessageItem : BaseEventItem val messageColorProvider: MessageColorProvider val itemLongClickListener: View.OnLongClickListener? val itemClickListener: ClickListener? - // val memberClickListener: ClickListener? val reactionPillCallback: TimelineEventController.ReactionPillCallback? 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 ba7865ac57..977a5b426a 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 @@ -67,6 +67,11 @@ abstract class AbsMessageItem : AbsBaseMessageItem } } + private val _threadClickListener = object : ClickListener { + override fun invoke(p1: View) { + attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false) + } + } override fun bind(holder: H) { super.bind(holder) if (attributes.informationData.showInformation) { @@ -107,6 +112,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem // Threads if(BuildConfig.THREADING_ENABLED) { + holder.threadSummaryConstraintLayout.onClick(_threadClickListener) attributes.threadDetails?.let { threadDetails -> holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() @@ -125,6 +131,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnClickListener(null) holder.memberNameView.setOnLongClickListener(null) + holder.threadSummaryConstraintLayout.setOnClickListener(null) super.unbind(holder) } @@ -156,6 +163,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem val memberClickListener: ClickListener? = null, override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null, + val threadCallback: TimelineEventController.ThreadCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null, val threadDetails: ThreadDetails? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index 4876e8e500..3693a5002e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -77,7 +77,8 @@ abstract class NoticeItem : BaseEventItem() { val noticeText: CharSequence, val itemLongClickListener: View.OnLongClickListener? = null, val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, - val avatarClickListener: ClickListener? = null + val avatarClickListener: ClickListener? = null, + val threadSummaryClickListener: ClickListener? = null ) companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index 007b419532..db052a42d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -19,9 +19,16 @@ package im.vector.app.features.home.room.threads import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.children +import androidx.fragment.app.FragmentTransaction import com.google.android.material.appbar.MaterialToolbar import im.vector.app.R import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity @@ -32,6 +39,7 @@ import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable { @@ -89,6 +97,31 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon threadTimelineArgs = threadTimelineArgs )) + /** + * This function is used to navigate to the selected thread timeline. + * One usage of that is from the Threads Activity + */ + fun navigateToThreadTimeline( + timelineEvent: TimelineEvent) { + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = timelineEvent.roomId, + displayName = timelineEvent.senderInfo.displayName, + avatarUrl = timelineEvent.senderInfo.avatarUrl, + rootThreadEventId = timelineEvent.eventId) + val commonOption: (FragmentTransaction) -> Unit = { + it.setCustomAnimations(R.anim.animation_slide_in_right, R.anim.animation_slide_out_left, R.anim.animation_slide_in_left, R.anim.animation_slide_out_right) + } + addFragmentToBackstack( + frameId = R.id.threadsActivityFragmentContainer, + fragmentClass = TimelineFragment::class.java, + params = TimelineArgs( + roomId = timelineEvent.roomId, + threadTimelineArgs = roomThreadDetailArgs + ), + option = commonOption + ) + } + override fun configure(toolbar: MaterialToolbar) { configureToolbar(toolbar) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt index 57cba163eb..8ed19a97c8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt @@ -16,13 +16,17 @@ package im.vector.app.features.home.room.threads.list.model +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -38,9 +42,11 @@ abstract class ThreadSummaryModel : VectorEpoxyModel( @EpoxyAttribute lateinit var lastMessage: String @EpoxyAttribute lateinit var lastMessageCounter: String @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null override fun bind(holder: Holder) { super.bind(holder) + holder.rootView.onClick(itemClickListener) avatarRenderer.render(matrixItem, holder.avatarImageView) holder.avatarImageView.contentDescription = matrixItem.getBestName() holder.titleTextView.text = title @@ -54,6 +60,7 @@ abstract class ThreadSummaryModel : VectorEpoxyModel( holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName() holder.lastMessageTextView.text = lastMessage holder.lastMessageCounterTextView.text = lastMessageCounter + } class Holder : VectorEpoxyHolder() { @@ -64,5 +71,6 @@ abstract class ThreadSummaryModel : VectorEpoxyModel( val lastMessageAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) val lastMessageCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) val lastMessageTextView by bind(R.id.messageThreadSummaryInfoTextView) + val rootView by bind(R.id.threadSummaryRootConstraintLayout) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt index a47d2b6c3b..7b7480092c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt @@ -21,6 +21,7 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.threads.list.model.threadSummary +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -67,11 +68,14 @@ class ThreadSummaryController @Inject constructor( lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) + itemClickListener { + host.listener?.onThreadClicked(timelineEvent) + } } } } interface Listener { - fun onBreadcrumbClicked(roomId: String) + fun onThreadClicked(timelineEvent: TimelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index d2551b58c1..eb732c44c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -20,7 +20,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.isVisible +import androidx.transition.TransitionInflater import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -33,10 +35,13 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsAnimator import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel +import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -45,7 +50,8 @@ class ThreadListFragment @Inject constructor( private val avatarRenderer: AvatarRenderer, private val threadSummaryController: ThreadSummaryController, val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory -) : VectorBaseFragment() { +) : VectorBaseFragment(), + ThreadSummaryController.Listener { private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel() @@ -65,15 +71,16 @@ class ThreadListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) initToolbar() views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false) -// threadSummaryController.listener = this + threadSummaryController.listener = this } override fun onDestroyView() { views.threadListRecyclerView.cleanup() -// breadcrumbsController.listener = null + threadSummaryController.listener = null super.onDestroyView() } - private fun initToolbar(){ + + private fun initToolbar() { setupToolbar(views.threadListToolbar) renderToolbar() } @@ -84,8 +91,13 @@ class ThreadListFragment @Inject constructor( private fun renderToolbar() { views.includeThreadListToolbar.roomToolbarThreadConstraintLayout.isVisible = true - val matrixItem = MatrixItem.RoomItem(threadListArgs.roomId, threadListArgs.displayName, threadListArgs.avatarUrl) - avatarRenderer.render(matrixItem, views.includeThreadListToolbar.roomToolbarThreadImageView) - views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName + val matrixItem = MatrixItem.RoomItem(threadListArgs.roomId, threadListArgs.displayName, threadListArgs.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadListToolbar.roomToolbarThreadImageView) + views.includeThreadListToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_list_title) + views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName + } + + override fun onThreadClicked(timelineEvent: TimelineEvent) { + (activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 02452cf6fc..37783d022b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -143,6 +143,7 @@ interface Navigator { fun openCallTransfer(context: Context, callId: String) fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) + fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) } diff --git a/vector/src/main/res/anim/animation_slide_in_left.xml b/vector/src/main/res/anim/animation_slide_in_left.xml new file mode 100644 index 0000000000..46547c691d --- /dev/null +++ b/vector/src/main/res/anim/animation_slide_in_left.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_in_right.xml b/vector/src/main/res/anim/animation_slide_in_right.xml new file mode 100644 index 0000000000..d0366bc633 --- /dev/null +++ b/vector/src/main/res/anim/animation_slide_in_right.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_out_left.xml b/vector/src/main/res/anim/animation_slide_out_left.xml new file mode 100644 index 0000000000..3d734533df --- /dev/null +++ b/vector/src/main/res/anim/animation_slide_out_left.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_out_right.xml b/vector/src/main/res/anim/animation_slide_out_right.xml new file mode 100644 index 0000000000..60a3f22721 --- /dev/null +++ b/vector/src/main/res/anim/animation_slide_out_right.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_thread_summary.xml b/vector/src/main/res/layout/item_thread_summary.xml index 130dae44b1..8cf93c5404 100644 --- a/vector/src/main/res/layout/item_thread_summary.xml +++ b/vector/src/main/res/layout/item_thread_summary.xml @@ -1,12 +1,18 @@ - + android:paddingEnd="0dp" + android:background="?android:colorBackground" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground"> + android:visibility="gone" + tools:visibility="visible"> + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/thread_timeline_title" /> + tools:text="Hello There, whats up! Its a large sentence whats up! Its a large centence" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e99cb880e1..316b24f4fb 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1029,7 +1029,8 @@ Filter Threads in room - Thread + Thread + Threads Reason for reporting this content From afc69c77bda302c845014bdddd0c5ce78191706f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 24 Nov 2021 18:23:33 +0200 Subject: [PATCH 014/130] Add local filtering in thread list --- .../sdk/api/session/events/model/Event.kt | 2 +- .../session/room/timeline/TimelineService.kt | 7 ++ .../database/helper/ThreadEventsHelper.kt | 17 ++++- .../internal/database/mapper/EventMapper.kt | 2 +- .../room/timeline/DefaultTimelineService.kt | 14 +++- .../list/viewmodel/ThreadSummaryController.kt | 16 +---- .../list/viewmodel/ThreadSummaryViewModel.kt | 39 ++++++----- .../list/viewmodel/ThreadSummaryViewState.kt | 3 +- .../list/views/ThreadListBottomSheet.kt | 69 +++++++++++++++++++ .../threads/list/views/ThreadListFragment.kt | 22 +++--- .../res/layout/bottom_sheet_thread_list.xml | 47 +++++++++++++ vector/src/main/res/values/strings.xml | 7 +- 12 files changed, 199 insertions(+), 46 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_thread_list.xml 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 77285dd463..621e525bd3 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 @@ -195,7 +195,7 @@ data class Event( * It can be used especially for message summaries. * It will return a decrypted text message or an empty string otherwise. */ - fun getDecryptedUserFriendlyTextSummary(): String { + fun getDecryptedTextSummary(): String { val text = getDecryptedValue().orEmpty() return when { isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index aa70343279..6b1ad5554b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -68,4 +68,11 @@ interface TimelineService { */ fun getAllThreads(): List + /** + * Returns whether or not the current user is participating in the thread + * @param rootThreadEventId the eventId of the current thread + */ + fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean + + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 755891af3e..aa3ba0fc25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -86,7 +86,6 @@ internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEv .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .findAll() - /** * Find all TimelineEventEntity that are root threads for the specified room * @param roomId The room that all stored root threads will be returned @@ -94,6 +93,20 @@ internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEv internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery = TimelineEventEntity .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD,true) + .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) +/** + * Returns whether or not the given user is participating in a current thread + * @param roomId the room that the thread exists + * @param rootThreadEventId the thread that the search will be done + * @param senderId the user that will try to find participation + */ +internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Realm, roomId: String, rootThreadEventId: String, senderId: String): Boolean = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .equalTo(TimelineEventEntityFields.ROOT.SENDER, senderId) + .findFirst() + ?.let { true } + ?: false 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 aded11e815..cf16138196 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 @@ -111,7 +111,7 @@ internal object EventMapper { avatarUrl = timelineEventEntity.senderAvatar ) }, - threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedUserFriendlyTextSummary().orEmpty() + threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 690f300827..2335f7bcd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.realm.Realm import io.realm.Sort import io.realm.kotlin.where import org.matrix.android.sdk.api.session.events.model.isImageMessage @@ -32,10 +33,10 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId +import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask @@ -111,10 +112,21 @@ internal class DefaultTimelineService @AssistedInject constructor( { timelineEventMapper.map(it) } ) } + override fun getAllThreads(): List { return monarchy.fetchAllMappedSync( { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { timelineEventMapper.map(it) } ) } + + override fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { + TimelineEventEntity.isUserParticipatingInThread( + realm = it, + roomId = roomId, + rootThreadEventId = rootThreadEventId, + senderId = senderId) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt index 7b7480092c..2e3b58bb77 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright 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. @@ -34,12 +34,6 @@ class ThreadSummaryController @Inject constructor( private var viewState: ThreadSummaryViewState? = null - init { - // We are requesting a model build directly as the first build of epoxy is on the main thread. - // It avoids to build the whole list of breadcrumbs on the main thread. - requestModelBuild() - } - fun update(viewState: ThreadSummaryViewState) { this.viewState = viewState requestModelBuild() @@ -48,13 +42,7 @@ class ThreadSummaryController @Inject constructor( override fun buildModels() { val safeViewState = viewState ?: return val host = this - // Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client -// zeroItem { -// id("top") -// } - // An empty breadcrumbs list can only be temporary because when entering in a room, - // this one is added to the breadcrumbs safeViewState.rootThreadEventList.invoke() ?.forEach { timelineEvent -> val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST) @@ -64,7 +52,7 @@ class ThreadSummaryController @Inject constructor( matrixItem(timelineEvent.senderInfo.toMatrixItem()) title(timelineEvent.senderInfo.displayName) date(date) - rootMessage(timelineEvent.root.getDecryptedUserFriendlyTextSummary()) + rootMessage(timelineEvent.root.getDecryptedTextSummary()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt index 385213470a..449090cc73 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright 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. @@ -26,16 +26,14 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.home.room.threads.list.views.ThreadListFragment -import org.matrix.android.sdk.api.query.QueryStringValue +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState, private val session: Session) : - VectorViewModel(initialState) { + VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) @@ -54,19 +52,28 @@ class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialSt } init { - observeThreadsSummary() + observeThreadsList(initialState.shouldFilterThreads) } - override fun handle(action: EmptyAction) { - // No op - } + override fun handle(action: EmptyAction) {} + private fun observeThreadsList(shouldFilterThreads: Boolean) = + room?.flow() + ?.liveThreadList() + ?.map { + if (!shouldFilterThreads) return@map it + it.filter { timelineEvent -> + room.isUserParticipatingInThread(timelineEvent.eventId, session.myUserId) + } + } + ?.flowOn(room.coroutineDispatchers.io) + ?.execute { asyncThreads -> + copy( + rootThreadEventList = asyncThreads, + shouldFilterThreads = shouldFilterThreads) + } - private fun observeThreadsSummary() { - room?.flow() - ?.liveThreadList() - ?.execute { asyncThreads -> - copy(rootThreadEventList = asyncThreads) - } + fun applyFiltering(shouldFilterThreads: Boolean) { + observeThreadsList(shouldFilterThreads) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt index b0c9c2ea26..13f1189708 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright 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. @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent data class ThreadSummaryViewState( val rootThreadEventList: Async> = Uninitialized, + val shouldFilterThreads: Boolean = false, val roomId: String ) : MavericksState{ diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt new file mode 100644 index 0000000000..b3938a10a8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt @@ -0,0 +1,69 @@ +/* + * 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 im.vector.app.features.home.room.threads.list.views + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.parentFragmentViewModel +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetThreadListBinding +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewState + +class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetThreadListBinding { + return BottomSheetThreadListBinding.inflate(inflater, container, false) + } + + + private val threadListViewModel: ThreadSummaryViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + threadListViewModel.subscribe(this){ + renderState(it) + } + views.threadListModalAllThreads.views.bottomSheetActionClickableZone.debouncedClicks { + threadListViewModel.applyFiltering(false) + dismiss() + } + views.threadListModalMyThreads.views.bottomSheetActionClickableZone.debouncedClicks { + threadListViewModel.applyFiltering(true) + dismiss() + } + + } + + private fun renderState(state: ThreadSummaryViewState) { + + if(state.shouldFilterThreads){ + views.threadListModalAllThreads.rightIcon = null + views.threadListModalMyThreads.rightIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick) + }else{ + views.threadListModalAllThreads.rightIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick) + views.threadListModalMyThreads.rightIcon = null + } + + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index eb732c44c9..0f34270481 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -18,11 +18,10 @@ package im.vector.app.features.home.room.threads.list.views import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.view.isVisible -import androidx.transition.TransitionInflater import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -32,21 +31,16 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentThreadListBinding import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsAnimator -import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel -import im.vector.app.features.home.room.detail.RoomDetailSharedActionViewModel +import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs -import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject class ThreadListFragment @Inject constructor( - private val session: Session, private val avatarRenderer: AvatarRenderer, private val threadSummaryController: ThreadSummaryController, val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory @@ -67,10 +61,20 @@ class ThreadListFragment @Inject constructor( super.onCreate(savedInstanceState) } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_thread_list_filter -> { + ThreadListBottomSheet().show(childFragmentManager, "Filtering") + true + } + else -> super.onOptionsItemSelected(item) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initToolbar() - views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false) + views.threadListRecyclerView.configureWith(threadSummaryController, TimelineItemAnimator(), hasFixedSize = false) threadSummaryController.listener = this } diff --git a/vector/src/main/res/layout/bottom_sheet_thread_list.xml b/vector/src/main/res/layout/bottom_sheet_thread_list.xml new file mode 100644 index 0000000000..3fd75e1823 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_thread_list.xml @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 316b24f4fb..ea7ce4cf84 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1027,10 +1027,15 @@ INVITED JOINED - + Filter Threads in room Thread Threads + Filter + All Threads + Shows all threads from current room + My Threads + Shows all threads you’ve participated in Reason for reporting this content From c4967a287144547de8bcba6ea77fa8cdf887530d Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 25 Nov 2021 17:59:28 +0200 Subject: [PATCH 015/130] Handle chunks merging with thread summary Add animation to fragment transition with offset for recyclerview initialization Support threads on deleted events --- .../database/helper/ChunkEntityHelper.kt | 13 +++++ .../database/query/EventEntityQueries.kt | 7 +++ .../room/timeline/TokenChunkEventPersistor.kt | 49 ------------------- .../timeline/factory/MessageItemFactory.kt | 12 ++--- .../detail/timeline/item/AbsMessageItem.kt | 15 +++--- ...readSummaryModel.kt => ThreadListModel.kt} | 5 +- ...yController.kt => ThreadListController.kt} | 10 ++-- ...aryViewModel.kt => ThreadListViewModel.kt} | 14 +++--- ...aryViewState.kt => ThreadListViewState.kt} | 2 +- .../list/views/ThreadListBottomSheet.kt | 25 +++------- .../threads/list/views/ThreadListFragment.kt | 22 ++++----- .../main/res/anim/animation_slide_in_left.xml | 3 +- .../res/anim/animation_slide_in_right.xml | 4 +- .../res/anim/animation_slide_out_left.xml | 4 +- .../res/anim/animation_slide_out_right.xml | 3 +- .../main/res/layout/fragment_thread_list.xml | 2 +- ...hread_summary.xml => item_thread_list.xml} | 0 17 files changed, 80 insertions(+), 110 deletions(-) rename vector/src/main/java/im/vector/app/features/home/room/threads/list/model/{ThreadSummaryModel.kt => ThreadListModel.kt} (95%) rename vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/{ThreadSummaryController.kt => ThreadListController.kt} (92%) rename vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/{ThreadSummaryViewModel.kt => ThreadListViewModel.kt} (81%) rename vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/{ThreadSummaryViewState.kt => ThreadListViewState.kt} (97%) rename vector/src/main/res/layout/{item_thread_summary.xml => item_thread_list.xml} (100%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index f74e4b0f4c..b0d15ce8da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import timber.log.Timber @@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt this.senderName = timelineEventEntity.senderName this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName } + handleThreadSummary(realm, eventId, copied) timelineEvents.add(copied) } +/** + * Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one + */ +private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) { + EventEntity + .whereRoomId(realm, newTimelineEventEntity.roomId) + .equalTo(EventEntityFields.IS_ROOT_THREAD, true) + .equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId) + .findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity +} + private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() ?: realm.createObject(eventEntity.eventId).apply { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index e27130442d..a439d6aae7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -49,6 +49,13 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu .equalTo(EventEntityFields.EVENT_ID, eventId) } + +internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery { + return realm.where() + .equalTo(EventEntityFields.ROOM_ID, roomId) +} + + internal fun EventEntity.Companion.where(realm: Realm, eventIds: List): RealmQuery { return realm.where() .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray()) 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 ba34e88ff7..f6441c9d60 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 @@ -270,53 +270,4 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() } - -// /** -// * Mark or update the thread root event accordingly. If the Threading is disabled -// * no action is done -// */ -// private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) { -// -// if (!BuildConfig.THREADING_ENABLED) return -// -// val rootThreadEventId = eventEntity.rootThreadEventId -// -// if (eventEntity.isThread && rootThreadEventId != null) { -// markEventAsRootEvent(realm, rootThreadEventId) -// } else { -// markAsRootEventIfNeeded(realm, eventEntity.eventId) -// } -// } - -// /** -// * Finds the event with rootThreadEventId and marks it as a root thread -// */ -// private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) { -// val rootThreadEvent = EventEntity -// .where(realm, rootThreadEventId) -// .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return -// rootThreadEvent.isThread = true -// } -// -// /** -// * Also check if there is at least one thread message for that rootThreadEventId, -// * that means it is a root thread so it should be updated accordingly -// */ -// private fun markAsRootEventIfNeeded(realm: Realm, candidateIdRootThread: String) { -// EventEntity -// .whereRootThreadEventId(realm, candidateIdRootThread) -// .findFirst() ?: return -// -// markEventAsRootEvent(realm, candidateIdRootThread) -// } - -// /** -// * 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/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 5e25b52473..57de9d7233 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 @@ -125,19 +125,19 @@ class MessageItemFactory @Inject constructor( pillsPostProcessorFactory.create(roomId) } - - - fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event val highlight = params.isHighlighted val callback = params.callback event.root.eventId ?: return null roomId = event.roomId val informationData = messageInformationDataFactory.create(params) + val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails + if (event.root.isRedacted()) { // message is redacted - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val attributes = messageItemAttributesFactory.create(null, informationData, callback, threadDetails) return buildRedactedItem(attributes, highlight) } @@ -154,7 +154,6 @@ class MessageItemFactory @Inject constructor( } // always hide summary when we are on thread timeline - val threadDetails = if(params.isFromThreadTimeline()) null else event.root.threadDetails val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails) // val all = event.root.toContent() @@ -180,9 +179,10 @@ class MessageItemFactory @Inject constructor( } } - private fun isFromThreadTimeline(params: TimelineItemFactoryParams){ + private fun isFromThreadTimeline(params: TimelineItemFactoryParams) { params.rootThreadEventId } + private fun buildOptionsMessageItem(messageContent: MessageOptionsContent, informationData: MessageInformationData, highlight: Boolean, 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 977a5b426a..903a650786 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 @@ -72,6 +72,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false) } } + override fun bind(holder: H) { super.bind(holder) if (attributes.informationData.showInformation) { @@ -111,26 +112,28 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA // Threads - if(BuildConfig.THREADING_ENABLED) { + if (BuildConfig.THREADING_ENABLED) { holder.threadSummaryConstraintLayout.onClick(_threadClickListener) attributes.threadDetails?.let { threadDetails -> holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage - threadDetails.threadSummarySenderInfo?.let { senderInfo -> - attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) - } - } ?: run{holder.threadSummaryConstraintLayout.isVisible = false} + + val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let + val displayName = threadDetails.threadSummarySenderInfo?.displayName + val avatarUrl = threadDetails.threadSummarySenderInfo?.avatarUrl + attributes.avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView) + } ?: run { holder.threadSummaryConstraintLayout.isVisible = false } } } - override fun unbind(holder: H) { attributes.avatarRenderer.clear(holder.avatarImageView) holder.avatarImageView.setOnClickListener(null) holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnClickListener(null) holder.memberNameView.setOnLongClickListener(null) + attributes.avatarRenderer.clear(holder.threadSummaryAvatarImageView) holder.threadSummaryConstraintLayout.setOnClickListener(null) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt index 8ed19a97c8..887a6acb4b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadSummaryModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.threads.list.model -import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout @@ -31,8 +30,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem -@EpoxyModelClass(layout = R.layout.item_thread_summary) -abstract class ThreadSummaryModel : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_thread_list) +abstract class ThreadListModel : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 2e3b58bb77..f0f9d1e9a2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -20,21 +20,21 @@ import com.airbnb.epoxy.EpoxyController import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.threads.list.model.threadSummary +import im.vector.app.features.home.room.threads.list.model.threadList import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject -class ThreadSummaryController @Inject constructor( +class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val dateFormatter: VectorDateFormatter ) : EpoxyController() { var listener: Listener? = null - private var viewState: ThreadSummaryViewState? = null + private var viewState: ThreadListViewState? = null - fun update(viewState: ThreadSummaryViewState) { + fun update(viewState: ThreadListViewState) { this.viewState = viewState requestModelBuild() } @@ -46,7 +46,7 @@ class ThreadSummaryController @Inject constructor( safeViewState.rootThreadEventList.invoke() ?.forEach { timelineEvent -> val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST) - threadSummary { + threadList { id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) matrixItem(timelineEvent.senderInfo.toMatrixItem()) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt similarity index 81% rename from vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 449090cc73..715478cec3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -31,23 +31,23 @@ import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.flow -class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState, - private val session: Session) : - VectorViewModel(initialState) { +class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState, + private val session: Session) : + VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) @AssistedFactory interface Factory { - fun create(initialState: ThreadSummaryViewState): ThreadSummaryViewModel + fun create(initialState: ThreadListViewState): ThreadListViewModel } - companion object : MavericksViewModelFactory { + companion object : MavericksViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ThreadSummaryViewState): ThreadSummaryViewModel? { + override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.threadSummaryViewModelFactory.create(state) + return fragment.threadListViewModelFactory.create(state) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt similarity index 97% rename from vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt index 13f1189708..01a5239aac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadSummaryViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt @@ -22,7 +22,7 @@ import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -data class ThreadSummaryViewState( +data class ThreadListViewState( val rootThreadEventList: Async> = Uninitialized, val shouldFilterThreads: Boolean = false, val roomId: String diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt index b3938a10a8..a4f40a820a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt @@ -20,14 +20,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.content.ContextCompat import com.airbnb.mvrx.parentFragmentViewModel import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetThreadListBinding -import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel -import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewState +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment() { @@ -35,13 +34,12 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment(), - ThreadSummaryController.Listener { + ThreadListController.Listener { - private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel() + private val threadListViewModel: ThreadListViewModel by fragmentViewModel() private val threadListArgs: ThreadListArgs by args() @@ -74,13 +74,13 @@ class ThreadListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initToolbar() - views.threadListRecyclerView.configureWith(threadSummaryController, TimelineItemAnimator(), hasFixedSize = false) - threadSummaryController.listener = this + views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) + threadListController.listener = this } override fun onDestroyView() { views.threadListRecyclerView.cleanup() - threadSummaryController.listener = null + threadListController.listener = null super.onDestroyView() } @@ -89,8 +89,8 @@ class ThreadListFragment @Inject constructor( renderToolbar() } - override fun invalidate() = withState(threadSummaryViewModel) { state -> - threadSummaryController.update(state) + override fun invalidate() = withState(threadListViewModel) { state -> + threadListController.update(state) } private fun renderToolbar() { diff --git a/vector/src/main/res/anim/animation_slide_in_left.xml b/vector/src/main/res/anim/animation_slide_in_left.xml index 46547c691d..77861c99f6 100644 --- a/vector/src/main/res/anim/animation_slide_in_left.xml +++ b/vector/src/main/res/anim/animation_slide_in_left.xml @@ -1,5 +1,6 @@ - \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_in_right.xml b/vector/src/main/res/anim/animation_slide_in_right.xml index d0366bc633..cf7488cc1a 100644 --- a/vector/src/main/res/anim/animation_slide_in_right.xml +++ b/vector/src/main/res/anim/animation_slide_in_right.xml @@ -1,5 +1,7 @@ - \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_out_left.xml b/vector/src/main/res/anim/animation_slide_out_left.xml index 3d734533df..2afa66ceab 100644 --- a/vector/src/main/res/anim/animation_slide_out_left.xml +++ b/vector/src/main/res/anim/animation_slide_out_left.xml @@ -1,5 +1,7 @@ - \ No newline at end of file diff --git a/vector/src/main/res/anim/animation_slide_out_right.xml b/vector/src/main/res/anim/animation_slide_out_right.xml index 60a3f22721..49348f1dac 100644 --- a/vector/src/main/res/anim/animation_slide_out_right.xml +++ b/vector/src/main/res/anim/animation_slide_out_right.xml @@ -1,5 +1,6 @@ - \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_thread_list.xml b/vector/src/main/res/layout/fragment_thread_list.xml index 25dd200737..be042a7bce 100644 --- a/vector/src/main/res/layout/fragment_thread_list.xml +++ b/vector/src/main/res/layout/fragment_thread_list.xml @@ -34,7 +34,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" android:background="?android:colorBackground" - tools:listitem="@layout/item_thread_summary" /> + tools:listitem="@layout/item_thread_list" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_thread_summary.xml b/vector/src/main/res/layout/item_thread_list.xml similarity index 100% rename from vector/src/main/res/layout/item_thread_summary.xml rename to vector/src/main/res/layout/item_thread_list.xml From 2a83e93265ae867ae850cb8aab75412b58f7360a Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 29 Nov 2021 15:00:27 +0200 Subject: [PATCH 016/130] Delete root message UI --- .../im/vector/app/core/extensions/TextView.kt | 4 ++++ .../room/threads/list/model/ThreadListModel.kt | 15 ++++++++++++--- .../list/viewmodel/ThreadListController.kt | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index adb655f169..c0911aec8b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -125,6 +125,10 @@ fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @AttrRes tintColor: Int? setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) } +fun TextView.clearDrawables() { + this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) +} + /** * Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt index 887a6acb4b..f47f6f46cc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt @@ -26,6 +26,9 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.clearDrawables +import im.vector.app.core.extensions.setLeftDrawable +import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -40,6 +43,7 @@ abstract class ThreadListModel : VectorEpoxyModel() { @EpoxyAttribute lateinit var rootMessage: String @EpoxyAttribute lateinit var lastMessage: String @EpoxyAttribute lateinit var lastMessageCounter: String + @EpoxyAttribute var rootMessageDeleted: Boolean = false @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: ClickListener? = null @@ -50,8 +54,14 @@ abstract class ThreadListModel : VectorEpoxyModel() { holder.avatarImageView.contentDescription = matrixItem.getBestName() holder.titleTextView.text = title holder.dateTextView.text = date - holder.rootMessageTextView.text = rootMessage - + if (rootMessageDeleted){ + holder.rootMessageTextView.text = holder.view.context.getString(R.string.event_redacted) + holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.colorOnPrimary) + holder.rootMessageTextView.compoundDrawablePadding = DimensionConverter(holder.view.context.resources).dpToPx(10) + }else{ + holder.rootMessageTextView.text = rootMessage + holder.rootMessageTextView.clearDrawables() + } // Last message summary lastMessageMatrixItem?.let { avatarRenderer.render(it, holder.lastMessageAvatarImageView) @@ -59,7 +69,6 @@ abstract class ThreadListModel : VectorEpoxyModel() { holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName() holder.lastMessageTextView.text = lastMessage holder.lastMessageCounterTextView.text = lastMessageCounter - } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index f0f9d1e9a2..d17dee6e51 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -52,6 +52,7 @@ class ThreadListController @Inject constructor( matrixItem(timelineEvent.senderInfo.toMatrixItem()) title(timelineEvent.senderInfo.displayName) date(date) + rootMessageDeleted(timelineEvent.root.isRedacted()) rootMessage(timelineEvent.root.getDecryptedTextSummary()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) From 53ca86dc6c5c1581adf557062db2dd4e39faacfb Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 29 Nov 2021 18:08:16 +0000 Subject: [PATCH 017/130] Permalink handling for thread events --- .../vector/app/features/home/HomeActivity.kt | 2 +- .../home/room/detail/TimelineFragment.kt | 45 ++++++++++++++----- .../home/room/threads/ThreadsActivity.kt | 11 ++++- .../features/navigation/DefaultNavigator.kt | 18 +++++--- .../app/features/navigation/Navigator.kt | 2 +- .../features/permalink/PermalinkHandler.kt | 35 ++++++++++++--- .../roomdirectory/PublicRoomsFragment.kt | 2 +- vector/src/main/res/values/strings.xml | 1 + 8 files changed, 89 insertions(+), 27 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index ff1154acc3..9b41cf52d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -556,7 +556,7 @@ class HomeActivity : return true } - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { if (roomId == null) return false MatrixToBottomSheet.withLink(deepLink.toString()) .show(supportFragmentManager, "HA#MatrixToBottomSheet") diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 1c37b26c60..7eeb6ae665 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -984,7 +984,12 @@ class TimelineFragment @Inject constructor( true } R.id.menu_thread_timeline_copy_link -> { - requireActivity().toast("menu_thread_timeline_copy_link") + getRootThreadEventId()?.let { + val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, it) + copyToClipboard(requireContext(), permalink, false) + showSnackWithMessage(getString(R.string.copied_to_clipboard)) + + } true } R.id.menu_thread_timeline_view_in_room -> { @@ -992,7 +997,10 @@ class TimelineFragment @Inject constructor( true } R.id.menu_thread_timeline_share -> { - requireActivity().toast("menu_thread_timeline_share") + getRootThreadEventId()?.let { + val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, it) + shareText(requireContext(), permalink) + } true } else -> super.onOptionsItemSelected(item) @@ -1649,20 +1657,36 @@ class TimelineFragment @Inject constructor( viewLifecycleOwner.lifecycleScope.launch { val isManaged = permalinkHandler .launch(requireActivity(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { // Same room? - if (roomId == timelineArgs.roomId) { - // Navigation to same room - if (eventId == null) { + if (roomId != timelineArgs.roomId) return false + // Navigation to same room + if (!isThreadTimeLine()) { + + if (rootThreadEventId != null) { + // Thread link, so PermalinkHandler will handle the navigation + return false + } + return if (eventId == null) { showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + true } else { // Highlight and scroll to this event roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + true + } + } else { + return if (rootThreadEventId == getRootThreadEventId() && eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_thread_when_already_in_the_thread)) + true + } else if (rootThreadEventId == getRootThreadEventId() && eventId != null) { + // we are in the same thread + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + true + } else { + false } - return true } - // Not handled - return false } override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { @@ -1816,7 +1840,6 @@ class TimelineFragment @Inject constructor( } } - override fun onAvatarClicked(informationData: MessageInformationData) { // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId)) openRoomMemberProfile(informationData.senderId) @@ -1862,7 +1885,7 @@ class TimelineFragment @Inject constructor( viewLifecycleOwner.lifecycleScope.launchWhenResumed { permalinkHandler .launch(requireContext(), url, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { requireActivity().finish() return false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index db052a42d3..fb1a6006c4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -94,6 +94,7 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon TimelineFragment::class.java, TimelineArgs( roomId = threadTimelineArgs.roomId, + eventId = getEventIdToNavigate(), threadTimelineArgs = threadTimelineArgs )) @@ -141,15 +142,23 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon private fun getThreadTimelineArgs(): ThreadTimelineArgs? = intent?.extras?.getParcelable(THREAD_TIMELINE_ARGS) private fun getThreadListArgs(): ThreadListArgs? = intent?.extras?.getParcelable(THREAD_LIST_ARGS) + private fun getEventIdToNavigate(): String? = intent?.extras?.getString(THREAD_EVENT_ID_TO_NAVIGATE) companion object { // private val FRAGMENT_TAG = RoomThreadDetailFragment::class.simpleName const val THREAD_TIMELINE_ARGS = "THREAD_TIMELINE_ARGS" + const val THREAD_EVENT_ID_TO_NAVIGATE = "THREAD_EVENT_ID_TO_NAVIGATE" const val THREAD_LIST_ARGS = "THREAD_LIST_ARGS" - fun newIntent(context: Context, threadTimelineArgs: ThreadTimelineArgs?, threadListArgs: ThreadListArgs?): Intent { + fun newIntent( + context: Context, + threadTimelineArgs: ThreadTimelineArgs?, + threadListArgs: ThreadListArgs?, + eventIdToNavigate: String? = null + ): Intent { return Intent(context, ThreadsActivity::class.java).apply { putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) + putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate) putExtra(THREAD_LIST_ARGS, threadListArgs) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 5f4a2168e9..3782a53d12 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -116,7 +116,12 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } - override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) { + override fun openRoom( + context: Context, + roomId: String, + eventId: String?, + buildTask: Boolean + ) { if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) { fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast()) return @@ -511,16 +516,19 @@ class DefaultNavigator @Inject constructor( } } - override fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) { + override fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String?) { context.startActivity(ThreadsActivity.newIntent( context = context, - threadTimelineArgs = threadTimelineArgs, - threadListArgs =null)) + threadTimelineArgs = threadTimelineArgs, + threadListArgs = null, + eventIdToNavigate = eventIdToNavigate + )) } + override fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) { context.startActivity(ThreadsActivity.newIntent( context = context, - threadTimelineArgs = null, + threadTimelineArgs = null, threadListArgs = ThreadListArgs( roomId = threadTimelineArgs.roomId, displayName = threadTimelineArgs.displayName, diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 37783d022b..d3a1a9eb03 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -142,7 +142,7 @@ interface Navigator { fun openCallTransfer(context: Context, callId: String) - fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) + fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index a02cfe7517..614a95af3b 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -21,12 +21,14 @@ import android.net.Uri import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.toast +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkService @@ -74,13 +76,27 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti return when (permalinkData) { is PermalinkData.RoomLink -> { val roomId = permalinkData.getRoomId() - if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink) != true) { + val session = activeSessionHolder.getSafeActiveSession() + + val rootThreadEventId = permalinkData.eventId?.let { eventId -> + val room = roomId?.let { session?.getRoom(it) } + // Root thread will be opened in timeline +// if(room?.getTimeLineEvent(eventId)?.root?.threadDetails?.isRootThread == true){ +// room.getTimeLineEvent(eventId)?.root?.eventId +// }else{ + room?.getTimeLineEvent(eventId)?.root?.getRootThreadEventId() +// } + + } + + if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink, rootThreadEventId) != true) { openRoom( context = context, roomId = roomId, permalinkData = permalinkData, rawLink = rawLink, - buildTask = buildTask + buildTask = buildTask, + rootThreadEventId = rootThreadEventId ) } true @@ -115,8 +131,8 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti private fun isPermalinkSupported(context: Context, url: String): Boolean { return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) || context.resources.getStringArray(R.array.permalink_supported_hosts).any { - url.startsWith(it) - } + url.startsWith(it) + } } private suspend fun PermalinkData.RoomLink.getRoomId(): String? { @@ -145,7 +161,8 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti roomId: String?, permalinkData: PermalinkData.RoomLink, rawLink: Uri, - buildTask: Boolean + buildTask: Boolean, + rootThreadEventId: String? =null ) { val session = activeSessionHolder.getSafeActiveSession() ?: return if (roomId == null) { @@ -155,6 +172,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti val roomSummary = session.getRoomSummary(roomId) val membership = roomSummary?.membership val eventId = permalinkData.eventId + // val roomAlias = permalinkData.getRoomAliasOrNull() val isSpace = roomSummary?.roomType == RoomType.SPACE return when { @@ -162,7 +180,10 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti membership?.isActive().orFalse() -> { if (!isSpace && membership == Membership.JOIN) { // If it's a room you're in, let's just open it, you can tap back if needed - navigator.openRoom(context, roomId, eventId, buildTask) + rootThreadEventId?.let { + val threadTimelineArgs = ThreadTimelineArgs(roomId, displayName = roomSummary.displayName, roomSummary.avatarUrl, it) + navigator.openThread(context, threadTimelineArgs, eventId) + } ?: navigator.openRoom(context, roomId, eventId, buildTask) } else { // maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined? navigator.openMatrixToBottomSheet(context, rawLink.toString()) @@ -187,7 +208,7 @@ interface NavigationInterceptor { /** * Return true if the navigation has been intercepted */ - fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri? = null): Boolean { + fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri? = null, rootThreadEventId: String? = null): Boolean { return false } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index b61583df55..fcc1d5fbf9 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -130,7 +130,7 @@ class PublicRoomsFragment @Inject constructor( val permalink = session.permalinkService().createPermalink(roomIdOrAlias) val isHandled = permalinkHandler .launch(requireContext(), permalink, object : NavigationInterceptor { - override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { + override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?, rootThreadEventId: String?): Boolean { requireActivity().finish() return false } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ea7ce4cf84..e9052ace75 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2259,6 +2259,7 @@ Matrix SDK Version Other third party notices You are already viewing this room! + You are already viewing this thread! Quick Reactions From e7b8b90b0a4fec24df7f1b406f9b63e41ca338b4 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 30 Nov 2021 16:05:45 +0000 Subject: [PATCH 018/130] Highlight the whole message along with the thread summary --- .../detail/timeline/item/AbsMessageItem.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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 903a650786..a9455a0e3c 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 @@ -18,14 +18,16 @@ package im.vector.app.features.home.room.detail.timeline.item import android.graphics.Typeface import android.view.View -import android.view.ViewStub import android.widget.ImageView +import android.widget.LinearLayout import android.widget.ProgressBar +import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.BuildConfig import im.vector.app.R @@ -37,7 +39,6 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.session.threads.ThreadDetails import org.matrix.android.sdk.api.util.MatrixItem -import timber.log.Timber /** * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state @@ -123,7 +124,21 @@ abstract class AbsMessageItem : AbsBaseMessageItem val displayName = threadDetails.threadSummarySenderInfo?.displayName val avatarUrl = threadDetails.threadSummarySenderInfo?.avatarUrl attributes.avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView) - } ?: run { holder.threadSummaryConstraintLayout.isVisible = false } + updateHighlightedMessageHeight(holder,true) + } ?: run { + holder.threadSummaryConstraintLayout.isVisible = false + updateHighlightedMessageHeight(holder,false) + } + } + } + + private fun updateHighlightedMessageHeight(holder: Holder, isExpanded: Boolean) { + holder.checkableBackground.updateLayoutParams { + if (isExpanded) { + addRule(RelativeLayout.ALIGN_BOTTOM, holder.threadSummaryConstraintLayout.id) + } else { + addRule(RelativeLayout.ALIGN_BOTTOM, holder.informationBottom.id) + } } } @@ -141,14 +156,15 @@ abstract class AbsMessageItem : AbsBaseMessageItem private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(informationData.matrixItem) abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { + val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) val sendStateImageView by bind(R.id.messageSendStateImageView) val eventSendingIndicator by bind(R.id.eventSendingIndicator) + val informationBottom by bind(R.id.informationBottom) val threadSummaryConstraintLayout by bind(R.id.messageThreadSummaryConstraintLayout) val threadSummaryCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) - val threadSummaryImageView by bind(R.id.messageThreadSummaryImageView) val threadSummaryAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) val threadSummaryInfoTextView by bind(R.id.messageThreadSummaryInfoTextView) } From 0241d66f8e98b37f3613e85c9fec9cb2d6f9d19b Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 1 Dec 2021 12:57:53 +0000 Subject: [PATCH 019/130] Enhance search functionality to support threads --- .../home/room/detail/TimelineFragment.kt | 7 +++++- .../home/room/detail/search/SearchFragment.kt | 24 +++++++++++++++---- .../detail/search/SearchResultController.kt | 4 ---- .../features/navigation/DefaultNavigator.kt | 9 ++++--- .../app/features/navigation/Navigator.kt | 3 +-- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 7eeb6ae665..a7db0af385 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1024,7 +1024,12 @@ class TimelineFragment @Inject constructor( private fun handleSearchAction() { if (session.getRoom(timelineArgs.roomId)?.isEncrypted() == false) { - navigator.openSearch(requireContext(), timelineArgs.roomId) + navigator.openSearch( + context = requireContext(), + roomId = timelineArgs.roomId, + roomDisplayName = roomDetailViewModel.getRoomSummary()?.displayName, + roomAvatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl + ) } else { showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 9f34cdd679..4b189095e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -37,13 +37,17 @@ import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSearchBinding +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import javax.inject.Inject @Parcelize data class SearchArgs( - val roomId: String + val roomId: String, + val roomDisplayName: String?, + val roomAvatarUrl: String? ) : Parcelable class SearchFragment @Inject constructor( @@ -112,10 +116,20 @@ class SearchFragment @Inject constructor( searchViewModel.handle(SearchAction.Retry) } - override fun onItemClicked(event: Event) { - event.roomId?.let { - navigator.openRoom(requireContext(), it, event.eventId) - } + override fun onItemClicked(event: Event) = + navigateToEvent(event) + + /** + * Navigate and highlight the event. If this is a thread event, + * user will be redirected to the appropriate thread room + * @param event the event to navigate and highlight + */ + private fun navigateToEvent(event: Event) { + val roomId = event.roomId ?: return + event.getRootThreadEventId()?.let { + val threadTimelineArgs = ThreadTimelineArgs(roomId, displayName = fragmentArgs.roomDisplayName, fragmentArgs.roomAvatarUrl, it) + navigator.openThread(requireContext(), threadTimelineArgs, event.eventId) + } ?: navigator.openRoom(requireContext(), roomId, event.eventId) } override fun loadMore() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index edef92ee2d..1b4d9faaec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -54,10 +54,6 @@ class SearchResultController @Inject constructor( fun loadMore() } - init { - setData(null) - } - override fun buildModels(data: SearchViewState?) { data ?: return diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 3782a53d12..0a062aae72 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -492,8 +492,11 @@ class DefaultNavigator @Inject constructor( } } - override fun openSearch(context: Context, roomId: String) { - val intent = SearchActivity.newIntent(context, SearchArgs(roomId)) + override fun openSearch(context: Context, + roomId: String, + roomDisplayName: String?, + roomAvatarUrl: String?) { + val intent = SearchActivity.newIntent(context, SearchArgs(roomId, roomDisplayName, roomAvatarUrl)) context.startActivity(intent) } @@ -522,7 +525,7 @@ class DefaultNavigator @Inject constructor( threadTimelineArgs = threadTimelineArgs, threadListArgs = null, eventIdToNavigate = eventIdToNavigate - )) + )) } override fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) { diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index d3a1a9eb03..a2edb27bb9 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -136,7 +136,7 @@ interface Navigator { inMemory: List = emptyList(), options: ((MutableList>) -> Unit)?) - fun openSearch(context: Context, roomId: String) + fun openSearch(context: Context, roomId: String, roomDisplayName: String?, roomAvatarUrl: String?) fun openDevTools(context: Context, roomId: String) @@ -145,5 +145,4 @@ interface Navigator { fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) - } From d1bb96cec047b9d546b6878d1dc8343007085693 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 3 Dec 2021 11:30:30 +0000 Subject: [PATCH 020/130] Threads notification badge UI --- .../src/main/res/values-v23/dimens.xml | 4 ++ .../ui-styles/src/main/res/values/dimens.xml | 4 ++ .../home/room/detail/RoomDetailViewModel.kt | 20 +++---- .../home/room/detail/TimelineFragment.kt | 41 ++++++++++++- .../main/res/drawable/notification_badge.xml | 12 ++++ .../layout/view_thread_notification_badge.xml | 58 +++++++++++++++++++ .../view_thread_notification_badge_old.xml | 53 +++++++++++++++++ vector/src/main/res/menu/menu_timeline.xml | 5 +- 8 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 library/ui-styles/src/main/res/values-v23/dimens.xml create mode 100644 vector/src/main/res/drawable/notification_badge.xml create mode 100644 vector/src/main/res/layout/view_thread_notification_badge.xml create mode 100644 vector/src/main/res/layout/view_thread_notification_badge_old.xml diff --git a/library/ui-styles/src/main/res/values-v23/dimens.xml b/library/ui-styles/src/main/res/values-v23/dimens.xml new file mode 100644 index 0000000000..18b8a81a7e --- /dev/null +++ b/library/ui-styles/src/main/res/values-v23/dimens.xml @@ -0,0 +1,4 @@ + + + 28dp + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index e2e50449ce..88c3d9d6e4 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -41,4 +41,8 @@ 320dp + 24dp + 48dp + 48dp + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1f5550b27f..907ca360bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -680,17 +680,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } else { when (itemId) { - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.open_matrix_apps -> true - R.id.voice_call -> state.isWebRTCCallOptionAvailable() - R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.open_matrix_apps -> true + R.id.voice_call -> state.isWebRTCCallOptionAvailable() + R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ - R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> true - R.id.threads -> BuildConfig.THREADING_ENABLED - R.id.dev_tools -> vectorPreferences.developerMode() - else -> false + R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined + R.id.search -> true + R.id.menu_timeline_thread_list -> BuildConfig.THREADING_ENABLED + R.id.dev_tools -> vectorPreferences.developerMode() + else -> false } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index a7db0af385..b37ba12f37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -36,12 +36,14 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri import androidx.core.text.buildSpannedString import androidx.core.text.toSpannable @@ -61,6 +63,7 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.glidePreloader import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -133,6 +136,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.UnreadMessagesSharedViewModel import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction @@ -285,6 +289,7 @@ class TimelineFragment @Inject constructor( private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() + private val debouncer = Debouncer(createUIHandler()) private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -907,6 +912,13 @@ class TimelineFragment @Inject constructor( (joinConfItem.actionView as? JoinConferenceView)?.onJoinClicked = { roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall) } + + // Custom thread notification menu item + menu.findItem(R.id.menu_timeline_thread_list)?.let { menuItem -> + menuItem.actionView.setOnClickListener { + onOptionsItemSelected(menuItem) + } + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -946,6 +958,10 @@ class TimelineFragment @Inject constructor( actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount") matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) } + + // Handle custom threads badge notification + updateMenuThreadNotificationBadge(menu, state) + } } @@ -971,7 +987,7 @@ class TimelineFragment @Inject constructor( callActionsHandler.onVideoCallClicked() true } - R.id.threads -> { + R.id.menu_timeline_thread_list -> { navigateToThreadList() true } @@ -1007,6 +1023,29 @@ class TimelineFragment @Inject constructor( } } + /** + * Update menu thread notification badge appropriately + */ + private fun updateMenuThreadNotificationBadge(menu: Menu, state: RoomDetailViewState) { + val menuThreadList = menu.findItem(R.id.menu_timeline_thread_list).actionView + val badgeFrameLayout = menuThreadList.findViewById(R.id.threadNotificationBadgeFrameLayout) + val badgeTextView = menuThreadList.findViewById(R.id.threadNotificationBadgeTextView) + + val unreadThreadMessages = 18 + state.pushCounter + + val userIsMentioned = true + if (unreadThreadMessages > 0) { + badgeFrameLayout.isVisible = true + badgeTextView.text = unreadThreadMessages.toString() + val badgeDrawable = DrawableCompat.wrap(badgeFrameLayout.background) + val color = ContextCompat.getColor(requireContext(), if (userIsMentioned) R.color.palette_vermilion else R.color.palette_gray_200) + DrawableCompat.setTint(badgeDrawable, color) + badgeFrameLayout.background = badgeDrawable + } else { + badgeFrameLayout.isVisible = false + } + } + /** * View and highlight the original root thread message in the main timeline */ diff --git a/vector/src/main/res/drawable/notification_badge.xml b/vector/src/main/res/drawable/notification_badge.xml new file mode 100644 index 0000000000..11f4b1d274 --- /dev/null +++ b/vector/src/main/res/drawable/notification_badge.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_notification_badge.xml b/vector/src/main/res/layout/view_thread_notification_badge.xml new file mode 100644 index 0000000000..8e2e098d7b --- /dev/null +++ b/vector/src/main/res/layout/view_thread_notification_badge.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_thread_notification_badge_old.xml b/vector/src/main/res/layout/view_thread_notification_badge_old.xml new file mode 100644 index 0000000000..70efceda51 --- /dev/null +++ b/vector/src/main/res/layout/view_thread_notification_badge_old.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 532b63dd38..6b334ab56a 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -38,14 +38,13 @@ tools:visible="true" /> - Date: Fri, 3 Dec 2021 18:15:25 +0000 Subject: [PATCH 021/130] Implement LOCAL thread notifications that work only on real time. --- .../org/matrix/android/sdk/flow/FlowRoom.kt | 7 +++++ .../session/room/timeline/TimelineService.kt | 17 +++++++++++ .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../database/RealmSessionStoreMigration.kt | 1 + .../database/helper/ThreadEventsHelper.kt | 27 +++++++++++++++-- .../internal/database/mapper/EventMapper.kt | 2 ++ .../internal/database/model/EventEntity.kt | 1 + .../room/timeline/DefaultTimelineService.kt | 25 ++++++++++++++++ .../room/timeline/TokenChunkEventPersistor.kt | 4 ++- .../sync/handler/room/RoomSyncHandler.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 26 +++++++++++++++++ .../home/room/detail/RoomDetailViewState.kt | 5 ++-- .../home/room/detail/TimelineFragment.kt | 4 +-- .../threads/list/model/ThreadListModel.kt | 5 ++++ .../list/viewmodel/ThreadListController.kt | 1 + .../src/main/res/layout/item_thread_list.xml | 29 ++++++++++++++----- 16 files changed, 141 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7091905991..cdb3bdf9c2 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -106,6 +106,13 @@ class FlowRoom(private val room: Room) { room.getAllThreads() } } + + fun liveLocalUnreadThreadList(): Flow> { + return room.getNumberOfLocalThreadNotificationsLive().asFlow() + .startWith(room.coroutineDispatchers.io) { + room.getNumberOfLocalThreadNotifications() + } + } } fun Room.flow(): FlowRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 6b1ad5554b..068fa87a66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -68,11 +68,28 @@ interface TimelineService { */ fun getAllThreads(): List + /** + * Get a live list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotificationsLive(): LiveData> + + /** + * Get a list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotifications(): List + /** * Returns whether or not the current user is participating in the thread * @param rootThreadEventId the eventId of the current thread */ fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean + /** + * Marks the current thread as read. This is a local implementation + * @param rootThreadEventId the eventId of the current thread + */ + suspend fun markThreadAsRead(rootThreadEventId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index 04dbb18797..62568cdce1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -22,5 +22,6 @@ data class ThreadDetails( val isRootThread: Boolean = false, val numberOfThreads: Int = 0, val threadSummarySenderInfo: SenderInfo? = null, - val threadSummaryLatestTextMessage: String? = null + val threadSummaryLatestTextMessage: String? = null, + val hasUnreadMessage: Boolean = false ) 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 111fc50e56..301a479d01 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 @@ -375,6 +375,7 @@ internal object RealmSessionStoreMigration : RealmMigration { ?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED) ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java) + ?.addField(EventEntityFields.HAS_UNREAD_THREAD_MESSAGES, Boolean::class.java) ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index aa3ba0fc25..34bc117ddf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -31,7 +31,7 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId * Finds the root thread event and update it with the latest message summary along with the number * of threads included. If there is no root thread event no action is done */ -internal fun Map.updateThreadSummaryIfNeeded() { +internal fun Map.updateThreadSummaryIfNeeded(isInitialSync: Boolean = false, currentUserId: String? = null) { if (!BuildConfig.THREADING_ENABLED) return @@ -47,6 +47,8 @@ internal fun Map.updateThreadSummaryIfNeeded() { val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( + isInitialSync = isInitialSync, + currentUserId = currentUserId, threadsCounted = it.size, latestMessageTimelineEventEntity = latestMessage ) @@ -68,11 +70,20 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? = /** * Mark or update the current event a root thread event */ -internal fun EventEntity.markEventAsRoot(threadsCounted: Int, - latestMessageTimelineEventEntity: TimelineEventEntity?) { +internal fun EventEntity.markEventAsRoot( + isInitialSync: Boolean, + currentUserId: String?, + threadsCounted: Int, + latestMessageTimelineEventEntity: TimelineEventEntity?) { isRootThread = true numberOfThreads = threadsCounted threadSummaryLatestMessage = latestMessageTimelineEventEntity + // skip notification coming from messages from the same user, also retain already marked events + hasUnreadThreadMessages = if (hasUnreadThreadMessages) { + latestMessageTimelineEventEntity?.root?.sender != currentUserId + } else { + if (latestMessageTimelineEventEntity?.root?.sender == currentUserId) false else !isInitialSync + } } /** @@ -96,6 +107,16 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) +/** + * Find the number of all the local notifications for the specified room + * @param roomId The room that the number of notifications will be returned + */ +internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) + .equalTo(TimelineEventEntityFields.ROOT.HAS_UNREAD_THREAD_MESSAGES, true) + /** * Returns whether or not the given user is participating in a current thread * @param roomId the room that the thread exists 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 cf16138196..319d91b12a 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 @@ -55,6 +55,7 @@ internal object EventMapper { eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorCode = event.mCryptoError?.name eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false + eventEntity.hasUnreadThreadMessages = event.threadDetails?.hasUnreadMessage ?: false eventEntity.rootThreadEventId = event.getRootThreadEventId() eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0 return eventEntity @@ -111,6 +112,7 @@ internal object EventMapper { avatarUrl = timelineEventEntity.senderAvatar ) }, + hasUnreadMessage = eventEntity.hasUnreadThreadMessages, threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() ) } 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 1898d63af8..1ba4d564bb 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 @@ -46,6 +46,7 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var isRootThread: Boolean = false, @Index var rootThreadEventId: String? = null, var numberOfThreads: Int = 0, + var hasUnreadThreadMessages: Boolean = false, var threadSummaryLatestMessage: TimelineEventEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 2335f7bcd2..3f702abde8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -32,9 +32,11 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where @@ -42,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, @@ -106,6 +109,20 @@ internal class DefaultTimelineService @AssistedInject constructor( } } + override fun getNumberOfLocalThreadNotificationsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getNumberOfLocalThreadNotifications(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + override fun getAllThreadsLive(): LiveData> { return monarchy.findAllMappedWithChanges( { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, @@ -129,4 +146,12 @@ internal class DefaultTimelineService @AssistedInject constructor( senderId = senderId) } } + + override suspend fun markThreadAsRead(rootThreadEventId: String) { + monarchy.awaitTransaction { + EventEntity.where( + realm = it, + eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false + } + } } 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 f6441c9d60..2fa298a171 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 @@ -267,7 +267,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } - optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() + // passing isInitialSync = true because we want to disable local notifications + // they do not work properly without the API + optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(true) } } 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 8d64c7fc96..8c258e7d91 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 @@ -425,7 +425,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } - optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() + optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(insertType == EventInsertType.INITIAL_SYNC, userId) // posting new events to timeline if any is registered timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 907ca360bb..cbe5e542fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -189,8 +189,12 @@ class RoomDetailViewModel @AssistedInject constructor( if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { prepareForEncryption() } + markThreadTimelineAsReadLocal() + observeLocalThreadNotifications() } + + private fun observeDataStore() { viewModelScope.launch { vectorDataStore.pushCounterFlow.collect { nbOfPush -> @@ -280,6 +284,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } + /** + * Observe local unread threads + */ + private fun observeLocalThreadNotifications(){ + room.flow() + .liveLocalUnreadThreadList() + .execute { + copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0) + } + + } fun getOtherUserIds() = room.roomSummary()?.otherMemberIds fun getRoomSummary() = room.roomSummary() @@ -1112,6 +1127,17 @@ class RoomDetailViewModel @AssistedInject constructor( } } + /** + * Mark the thread as read, while the user navigated within the thread + * This is a local implementation has nothing to do with APIs + */ + private fun markThreadTimelineAsReadLocal(){ + initialState.rootThreadEventId?.let{ + session.coroutineScope.launch { + room.markThreadAsRead(it) + } + } + } override fun onTimelineUpdated(snapshot: List) { timelineEvents.tryEmit(snapshot) 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 fa772ca073..df6c75d30c 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 @@ -67,8 +67,9 @@ data class RoomDetailViewState( val isAllowedToStartWebRTCCall: Boolean = true, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState(), - val rootThreadEventId: String? = null - ) : MavericksState { + val rootThreadEventId: String? = null, + val numberOfLocalUnreadThreads: Int = 0 +) : MavericksState { constructor(args: TimelineArgs) : this( roomId = args.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b37ba12f37..f12ca9e84c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1031,9 +1031,9 @@ class TimelineFragment @Inject constructor( val badgeFrameLayout = menuThreadList.findViewById(R.id.threadNotificationBadgeFrameLayout) val badgeTextView = menuThreadList.findViewById(R.id.threadNotificationBadgeTextView) - val unreadThreadMessages = 18 + state.pushCounter + val unreadThreadMessages = state.numberOfLocalUnreadThreads + val userIsMentioned = false - val userIsMentioned = true if (unreadThreadMessages > 0) { badgeFrameLayout.isVisible = true badgeTextView.text = unreadThreadMessages.toString() diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt index f47f6f46cc..f3aac46ed3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.threads.list.model import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -42,6 +43,7 @@ abstract class ThreadListModel : VectorEpoxyModel() { @EpoxyAttribute lateinit var date: String @EpoxyAttribute lateinit var rootMessage: String @EpoxyAttribute lateinit var lastMessage: String + @EpoxyAttribute var unreadMessage: Boolean = false @EpoxyAttribute lateinit var lastMessageCounter: String @EpoxyAttribute var rootMessageDeleted: Boolean = false @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null @@ -69,6 +71,7 @@ abstract class ThreadListModel : VectorEpoxyModel() { holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName() holder.lastMessageTextView.text = lastMessage holder.lastMessageCounterTextView.text = lastMessageCounter + holder.unreadImageView.isVisible = unreadMessage } class Holder : VectorEpoxyHolder() { @@ -79,6 +82,8 @@ abstract class ThreadListModel : VectorEpoxyModel() { val lastMessageAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView) val lastMessageCounterTextView by bind(R.id.messageThreadSummaryCounterTextView) val lastMessageTextView by bind(R.id.messageThreadSummaryInfoTextView) + val unreadImageView by bind(R.id.threadSummaryUnreadImageView) + val rootView by bind(R.id.threadSummaryRootConstraintLayout) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index d17dee6e51..6e07f0a95f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -53,6 +53,7 @@ class ThreadListController @Inject constructor( title(timelineEvent.senderInfo.displayName) date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) + unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false) rootMessage(timelineEvent.root.getDecryptedTextSummary()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) diff --git a/vector/src/main/res/layout/item_thread_list.xml b/vector/src/main/res/layout/item_thread_list.xml index 8cf93c5404..6a1d075b7c 100644 --- a/vector/src/main/res/layout/item_thread_list.xml +++ b/vector/src/main/res/layout/item_thread_list.xml @@ -1,18 +1,17 @@ - + android:foreground="?attr/selectableItemBackground" + android:paddingStart="12dp" + android:paddingTop="12dp" + android:paddingEnd="0dp"> + + Date: Thu, 9 Dec 2021 16:33:11 +0200 Subject: [PATCH 022/130] Add/Fix local echo to threads timeline --- .../room/relation/DefaultRelationService.kt | 32 +++++++++++-------- .../session/room/timeline/DefaultTimeline.kt | 6 +++- 2 files changed, 24 insertions(+), 14 deletions(-) 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 23862ae963..3852dd50b8 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 @@ -168,25 +168,31 @@ internal class DefaultRelationService @AssistedInject constructor( msgType: String, autoMarkdown: Boolean, formattedText: String?, - eventReplied: TimelineEvent?): Cancelable { - val event = eventReplied?.let { + eventReplied: TimelineEvent?): Cancelable? { + + val event = if (eventReplied != null) { eventFactory.createReplyTextEvent( roomId = roomId, eventReplied = eventReplied, replyText = replyInThreadText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId) - } ?: eventFactory.createThreadTextEvent( - rootThreadEventId = rootThreadEventId, - roomId = roomId, - text = replyInThreadText.toString(), - msgType = msgType, - autoMarkdown = autoMarkdown, - formattedText = formattedText) -// .also { -// saveLocalEcho(it) -// } - return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + ?.also { + saveLocalEcho(it) + } ?: return null + } else { + eventFactory.createThreadTextEvent( + rootThreadEventId = rootThreadEventId, + roomId = roomId, + text = replyInThreadText.toString(), + msgType = msgType, + autoMarkdown = autoMarkdown, + formattedText = formattedText) + .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/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 4d417fddbb..fe121090a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -448,7 +448,11 @@ internal class DefaultTimeline( */ private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { // If changeSet has deletion we are having a gap, so we clear everything - if (changeSet.deletionRanges.isNotEmpty()) { + // I observed there is a problem with this implementation in the threads timeline upon receiving + // a local echo, after adding && !isFromThreadTimeline below fixed the issue. + // Maybe there is a deeper problem here even on the main timeline + + if (changeSet.deletionRanges.isNotEmpty() && !isFromThreadTimeline) { clearAllValues() } var postSnapshot = false From 57ef0b59ab10bae0ba22009c9b12cb8b239a6cec Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 9 Dec 2021 20:29:13 +0200 Subject: [PATCH 023/130] Disable local echo for normal messages while there is a duplication --- .../session/room/relation/DefaultRelationService.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 3852dd50b8..4500c71e59 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 @@ -177,9 +177,10 @@ internal class DefaultRelationService @AssistedInject constructor( replyText = replyInThreadText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId) - ?.also { - saveLocalEcho(it) - } ?: return null +// ?.also { +// saveLocalEcho(it) +// } + ?: return null } else { eventFactory.createThreadTextEvent( rootThreadEventId = rootThreadEventId, @@ -188,9 +189,9 @@ internal class DefaultRelationService @AssistedInject constructor( msgType = msgType, autoMarkdown = autoMarkdown, formattedText = formattedText) - .also { - saveLocalEcho(it) - } +// .also { +// saveLocalEcho(it) +// } } return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } From 5c015a7444228537ec20d6dad6c4c8cb1e8c64ab Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 10 Dec 2021 20:15:39 +0200 Subject: [PATCH 024/130] Support stickers in threads --- .../home/room/detail/RoomDetailViewModel.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index cbe5e542fb..ec0caa7b6d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -75,6 +75,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent @@ -87,6 +88,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService @@ -193,8 +195,6 @@ class RoomDetailViewModel @AssistedInject constructor( observeLocalThreadNotifications() } - - private fun observeDataStore() { viewModelScope.launch { vectorDataStore.pushCounterFlow.collect { nbOfPush -> @@ -287,14 +287,14 @@ class RoomDetailViewModel @AssistedInject constructor( /** * Observe local unread threads */ - private fun observeLocalThreadNotifications(){ + private fun observeLocalThreadNotifications() { room.flow() .liveLocalUnreadThreadList() .execute { copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0) } - } + fun getOtherUserIds() = room.roomSummary()?.otherMemberIds fun getRoomSummary() = room.roomSummary() @@ -448,7 +448,10 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleSendSticker(action: RoomDetailAction.SendSticker) { - room.sendEvent(EventType.STICKER, action.stickerContent.toContent()) + val content = initialState.rootThreadEventId?.let { + action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.THREAD, it)) + } ?: action.stickerContent + room.sendEvent(EventType.STICKER, content.toContent()) } private fun handleStartCall(action: RoomDetailAction.StartCall) { @@ -1131,13 +1134,14 @@ class RoomDetailViewModel @AssistedInject constructor( * Mark the thread as read, while the user navigated within the thread * This is a local implementation has nothing to do with APIs */ - private fun markThreadTimelineAsReadLocal(){ - initialState.rootThreadEventId?.let{ + private fun markThreadTimelineAsReadLocal() { + initialState.rootThreadEventId?.let { session.coroutineScope.launch { room.markThreadAsRead(it) } } } + override fun onTimelineUpdated(snapshot: List) { timelineEvents.tryEmit(snapshot) From d56281dca7e31da4e52dfcc52f352e47c51e5a28 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 14 Dec 2021 13:35:08 +0200 Subject: [PATCH 025/130] - Enhance local notification to work with read receipt & the latest chunk - Local notification mentioning system - Fix/Improve thread list filtering --- .../session/room/timeline/TimelineService.kt | 2 +- .../sdk/api/session/threads/ThreadDetails.kt | 6 +- .../threads/ThreadNotificationBadgeState.kt | 25 ++++ .../threads/ThreadNotificationState.kt | 34 +++++ .../session/threads/ThreadTimelineEvent.kt | 28 ++++ .../database/RealmSessionStoreMigration.kt | 2 +- .../database/helper/ThreadEventsHelper.kt | 136 ++++++++++++++++-- .../internal/database/mapper/EventMapper.kt | 6 +- .../internal/database/model/EventEntity.kt | 12 +- .../session/room/timeline/DefaultTimeline.kt | 2 +- .../room/timeline/DefaultTimelineService.kt | 9 +- .../room/timeline/TokenChunkEventPersistor.kt | 17 +-- .../sync/handler/room/RoomSyncHandler.kt | 5 +- .../home/room/detail/RoomDetailViewModel.kt | 12 +- .../home/room/detail/RoomDetailViewState.kt | 3 +- .../home/room/detail/TimelineFragment.kt | 4 +- .../threads/list/model/ThreadListModel.kt | 28 +++- .../list/viewmodel/ThreadListController.kt | 12 +- .../list/viewmodel/ThreadListViewModel.kt | 34 ++--- .../list/viewmodel/ThreadListViewState.kt | 4 +- 20 files changed, 318 insertions(+), 63 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 068fa87a66..4ac4aab4e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -84,7 +84,7 @@ interface TimelineService { * Returns whether or not the current user is participating in the thread * @param rootThreadEventId the eventId of the current thread */ - fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean + fun isUserParticipatingInThread(rootThreadEventId: String): Boolean /** * Marks the current thread as read. This is a local implementation diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index 62568cdce1..ad6e139d01 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.threads import org.matrix.android.sdk.api.session.room.sender.SenderInfo +/** + * This class contains all the details needed for threads. + * Is is mainly used from within an Event. + */ data class ThreadDetails( val isRootThread: Boolean = false, val numberOfThreads: Int = 0, val threadSummarySenderInfo: SenderInfo? = null, val threadSummaryLatestTextMessage: String? = null, - val hasUnreadMessage: Boolean = false + var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt new file mode 100644 index 0000000000..8e861e73de --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationBadgeState.kt @@ -0,0 +1,25 @@ +/* + * 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.threads + +/** + * This class defines the state of a thread notification badge + */ +data class ThreadNotificationBadgeState( + val numberOfLocalUnreadThreads: Int = 0, + val isUserMentioned: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt new file mode 100644 index 0000000000..093e4a7627 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt @@ -0,0 +1,34 @@ +/* + * 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.threads + +/** + * This class defines the state of a thread notification + */ +enum class ThreadNotificationState { + + // There are no new message + NO_NEW_MESSAGE, + + // There is at least one new message + NEW_MESSAGE, + + // The is at least one new message that should bi highlighted + // ex. "Hello @aris.kotsomitopoulos" + NEW_HIGHLIGHTED_MESSAGE; + +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt new file mode 100644 index 0000000000..7b433566b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadTimelineEvent.kt @@ -0,0 +1,28 @@ +/* + * 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.threads + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This class contains a thread TimelineEvent along with a boolean that + * determines if the current user has participated in that event + */ +data class ThreadTimelineEvent( + val timelineEvent: TimelineEvent, + val isParticipating: Boolean +) 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 301a479d01..88fdb1c471 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 @@ -375,7 +375,7 @@ internal object RealmSessionStoreMigration : RealmMigration { ?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED) ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java) - ?.addField(EventEntityFields.HAS_UNREAD_THREAD_MESSAGES, Boolean::class.java) + ?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java) ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 34bc117ddf..32184c0ae9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -21,9 +21,14 @@ import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.internal.database.mapper.asDomain +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.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.findIncludingEvent import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId @@ -31,7 +36,7 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId * Finds the root thread event and update it with the latest message summary along with the number * of threads included. If there is no root thread event no action is done */ -internal fun Map.updateThreadSummaryIfNeeded(isInitialSync: Boolean = false, currentUserId: String? = null) { +internal fun Map.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) { if (!BuildConfig.THREADING_ENABLED) return @@ -47,13 +52,14 @@ internal fun Map.updateThreadSummaryIfNeeded(isInitialSync: val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( - isInitialSync = isInitialSync, - currentUserId = currentUserId, threadsCounted = it.size, latestMessageTimelineEventEntity = latestMessage ) + } } + + updateNotificationsNew(roomId, realm, currentUserId) } /** @@ -71,19 +77,11 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? = * Mark or update the current event a root thread event */ internal fun EventEntity.markEventAsRoot( - isInitialSync: Boolean, - currentUserId: String?, threadsCounted: Int, latestMessageTimelineEventEntity: TimelineEventEntity?) { isRootThread = true numberOfThreads = threadsCounted threadSummaryLatestMessage = latestMessageTimelineEventEntity - // skip notification coming from messages from the same user, also retain already marked events - hasUnreadThreadMessages = if (hasUnreadThreadMessages) { - latestMessageTimelineEventEntity?.root?.sender != currentUserId - } else { - if (latestMessageTimelineEventEntity?.root?.sender == currentUserId) false else !isInitialSync - } } /** @@ -115,7 +113,9 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) - .equalTo(TimelineEventEntityFields.ROOT.HAS_UNREAD_THREAD_MESSAGES, true) + .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name) + .or() + .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name) /** * Returns whether or not the given user is participating in a current thread @@ -131,3 +131,115 @@ internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Re .findFirst() ?.let { true } ?: false + +/** + * Returns whether or not the given user is mentioned in a current thread + * @param roomId the room that the thread exists + * @param rootThreadEventId the thread that the search will be done + * @param userId the user that will try to find if there is a mention + */ +internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean = + TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .equalTo(TimelineEventEntityFields.ROOT.SENDER, userId) + .findAll() + .firstOrNull { isUserMentioned(userId, it) } + ?.let { true } + ?: false + +/** + * Find the read receipt for the current user + */ +internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? = + ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) + .findFirst() + ?.eventId + +/** + * Returns whether or not the user is mentioned in the event + */ +internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean { + val decryptedContent = timelineEventEntity?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() + return decryptedContent.contains(currentUserId.replace("@", "").substringBefore(":")) +} + +/** + * Update badge notifications. Count the number of new thread events after the latest + * read receipt and aggregate. This function will find and notify new thread events + * that the user is either mentioned, or the user had participated in. + * Important: If the root thread event is not fetched notification will not work + * Important: It will work only with the latest chunk, while read marker will be changed + * immediately so we should not display wrong notifications + */ +internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) { + + val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return + + val readReceiptChunk = ChunkEntity + .findIncludingEvent(realm, readReceipt) ?: return + + val readReceiptChunkTimelineEvents = readReceiptChunk + .timelineEvents + .where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .findAll() ?: return + + val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt } + + if (readReceiptChunkPosition != -1 && readReceiptChunkPosition != readReceiptChunkTimelineEvents.lastIndex) { + // If the read receipt is found inside the chunk + + val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents + .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex) + .filter { it.root?.isThread() == true } + + // In order for the below code to work for old events, we should save the previous read receipt + // and then continue with the chunk search for that read receipt + /* + val newThreadEventsList = arrayListOf() + newThreadEventsList.addAll(threadEventsAfterReadReceipt) + + // got from latest chunk all new threads, lets move to the others + var nextChunk = ChunkEntity + .find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken) + .takeIf { readReceiptChunk.nextToken != null } + while (nextChunk != null) { + newThreadEventsList.addAll(nextChunk.timelineEvents + .filter { it.root?.isThread() == true }) + nextChunk = ChunkEntity + .find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken) + .takeIf { readReceiptChunk.nextToken != null } + }*/ + + // Find if the user is mentioned in those events + val userMentionsList = threadEventsAfterReadReceipt + .filter { + isUserMentioned(currentUserId = currentUserId, it) + }.map { + it.root?.rootThreadEventId + } + + // Find the root events in the new thread events + val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId } + + // Update root thread events only if the user have participated in + rootThreads.forEach { eventId -> + val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( + realm = realm, + roomId = roomId, + rootThreadEventId = eventId, + senderId = currentUserId) + val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst() + + if (isUserParticipating) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE + } + + if (userMentionsList.contains(eventId)) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE + } + } + } +} 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 319d91b12a..53925b1a8f 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 @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.threads.ThreadDetails +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.di.MoshiProvider @@ -55,9 +56,9 @@ internal object EventMapper { eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorCode = event.mCryptoError?.name eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false - eventEntity.hasUnreadThreadMessages = event.threadDetails?.hasUnreadMessage ?: false eventEntity.rootThreadEventId = event.getRootThreadEventId() eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0 + eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE return eventEntity } @@ -100,7 +101,6 @@ internal object EventMapper { MXCryptoError.ErrorType.valueOf(errorCode) } it.mCryptoErrorReason = eventEntity.decryptionErrorReason - it.threadDetails = ThreadDetails( isRootThread = eventEntity.isRootThread, numberOfThreads = eventEntity.numberOfThreads, @@ -112,7 +112,7 @@ internal object EventMapper { avatarUrl = timelineEventEntity.senderAvatar ) }, - hasUnreadMessage = eventEntity.hasUnreadThreadMessages, + threadNotificationState = eventEntity.threadNotificationState, threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() ) } 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 1ba4d564bb..b8e3de8681 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import io.realm.annotations.Index import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider @@ -46,7 +47,7 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var isRootThread: Boolean = false, @Index var rootThreadEventId: String? = null, var numberOfThreads: Int = 0, - var hasUnreadThreadMessages: Boolean = false, +// var threadNotificationState: Boolean = false, var threadSummaryLatestMessage: TimelineEventEntity? = null ) : RealmObject() { @@ -61,6 +62,15 @@ internal open class EventEntity(@Index var eventId: String = "", sendStateStr = value.name } + private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name + var threadNotificationState: ThreadNotificationState + get() { + return ThreadNotificationState.valueOf(threadNotificationStateStr) + } + set(value) { + threadNotificationStateStr = value.name + } + companion object fun setDecryptionResult(result: MXEventDecryptionResult) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index fe121090a0..707fe487ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -169,7 +169,7 @@ internal class DefaultTimeline( .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it) .or() - .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId) + .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, it) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .findAll() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 3f702abde8..95fb5f8595 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId @@ -41,6 +42,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.task.TaskExecutor @@ -48,6 +50,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, + @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val realmSessionProvider: RealmSessionProvider, private val timelineInput: TimelineInput, @@ -137,13 +140,13 @@ internal class DefaultTimelineService @AssistedInject constructor( ) } - override fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean { + override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { TimelineEventEntity.isUserParticipatingInThread( realm = it, roomId = roomId, rootThreadEventId = rootThreadEventId, - senderId = senderId) + senderId = userId) } } @@ -151,7 +154,7 @@ internal class DefaultTimelineService @AssistedInject constructor( monarchy.awaitTransaction { EventEntity.where( realm = it, - eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false + eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE } } } 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 2fa298a171..8331d3f5f4 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,10 +18,7 @@ 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.BuildConfig 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 @@ -44,8 +41,8 @@ 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.getOrCreate import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.database.query.whereRootThreadEventId import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber @@ -54,7 +51,9 @@ import javax.inject.Inject /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { +internal class TokenChunkEventPersistor @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String) { /** *
@@ -213,7 +212,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
         }
         val eventIds = ArrayList(eventList.size)
 
-        val optimizedThreadSummaryMap = hashMapOf()
+        val optimizedThreadSummaryMap = hashMapOf()
         eventList.forEach { event ->
             if (event.eventId == null || event.senderId == null) {
                 return@forEach
@@ -260,16 +259,12 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
         val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
                 (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
         if (shouldUpdateSummary) {
-            // TODO maybe add support to view latest thread message
             roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
         }
         if (currentChunk.isValid) {
             RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
         }
 
-        // passing isInitialSync = true because we want to disable local notifications
-        // they do not work properly without the API
-        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(true)
-
+        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
     }
 }
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 8c258e7d91..9f080e0648 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
@@ -425,7 +425,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
         }
 
-        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(insertType == EventInsertType.INITIAL_SYNC, userId)
+        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                roomId = roomId,
+                realm = realm,
+                currentUserId = userId)
 
         // posting new events to timeline if any is registered
         timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index ec0caa7b6d..88be46bd0b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -94,6 +94,8 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.session.widgets.model.WidgetType
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.flow.flow
@@ -291,7 +293,14 @@ class RoomDetailViewModel @AssistedInject constructor(
         room.flow()
                 .liveLocalUnreadThreadList()
                 .execute {
-                    copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0)
+                    val threadList = it.invoke()
+                    val isUserMentioned = threadList?.firstOrNull { timelineEvent ->
+                        timelineEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
+                    }?.let { true } ?: false
+                    val numberOfLocalUnreadThreads = threadList?.size ?: 0
+                    copy(threadNotificationBadgeState = ThreadNotificationBadgeState(
+                            numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
+                            isUserMentioned = isUserMentioned))
                 }
     }
 
@@ -1178,6 +1187,7 @@ class RoomDetailViewModel @AssistedInject constructor(
         chatEffectManager.delegate = null
         chatEffectManager.dispose()
         callManager.removeProtocolsCheckerListener(this)
+        markThreadTimelineAsReadLocal()
         super.onCleared()
     }
 }
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 df6c75d30c..051c9b6500 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
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.sync.SyncState
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
 import org.matrix.android.sdk.api.session.widgets.model.Widget
 import org.matrix.android.sdk.api.session.widgets.model.WidgetType
 
@@ -68,7 +69,7 @@ data class RoomDetailViewState(
         val hasFailedSending: Boolean = false,
         val jitsiState: JitsiState = JitsiState(),
         val rootThreadEventId: String? = null,
-        val numberOfLocalUnreadThreads: Int = 0
+        val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState()
 ) : MavericksState {
 
     constructor(args: TimelineArgs) : this(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index f12ca9e84c..92a8acf0cf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1031,8 +1031,8 @@ class TimelineFragment @Inject constructor(
         val badgeFrameLayout = menuThreadList.findViewById(R.id.threadNotificationBadgeFrameLayout)
         val badgeTextView = menuThreadList.findViewById(R.id.threadNotificationBadgeTextView)
 
-        val unreadThreadMessages = state.numberOfLocalUnreadThreads
-        val userIsMentioned = false
+        val unreadThreadMessages = state.threadNotificationBadgeState.numberOfLocalUnreadThreads
+        val userIsMentioned = state.threadNotificationBadgeState.isUserMentioned
 
         if (unreadThreadMessages > 0) {
             badgeFrameLayout.isVisible = true
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
index f3aac46ed3..286f027915 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.threads.list.model
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
@@ -32,6 +33,7 @@ import im.vector.app.core.extensions.setLeftDrawable
 import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.features.displayname.getBestName
 import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.util.MatrixItem
 
 @EpoxyModelClass(layout = R.layout.item_thread_list)
@@ -43,7 +45,7 @@ abstract class ThreadListModel : VectorEpoxyModel() {
     @EpoxyAttribute lateinit var date: String
     @EpoxyAttribute lateinit var rootMessage: String
     @EpoxyAttribute lateinit var lastMessage: String
-    @EpoxyAttribute  var unreadMessage: Boolean = false
+    @EpoxyAttribute var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
     @EpoxyAttribute lateinit var lastMessageCounter: String
     @EpoxyAttribute var rootMessageDeleted: Boolean = false
     @EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
@@ -56,11 +58,11 @@ abstract class ThreadListModel : VectorEpoxyModel() {
         holder.avatarImageView.contentDescription = matrixItem.getBestName()
         holder.titleTextView.text = title
         holder.dateTextView.text = date
-        if (rootMessageDeleted){
+        if (rootMessageDeleted) {
             holder.rootMessageTextView.text = holder.view.context.getString(R.string.event_redacted)
             holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.colorOnPrimary)
             holder.rootMessageTextView.compoundDrawablePadding = DimensionConverter(holder.view.context.resources).dpToPx(10)
-        }else{
+        } else {
             holder.rootMessageTextView.text = rootMessage
             holder.rootMessageTextView.clearDrawables()
         }
@@ -71,7 +73,24 @@ abstract class ThreadListModel : VectorEpoxyModel() {
         holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
         holder.lastMessageTextView.text = lastMessage
         holder.lastMessageCounterTextView.text = lastMessageCounter
-        holder.unreadImageView.isVisible = unreadMessage
+        renderNotificationState(holder)
+    }
+
+    private fun renderNotificationState(holder: Holder) {
+
+        when (threadNotificationState) {
+            ThreadNotificationState.NEW_MESSAGE             -> {
+                holder.unreadImageView.isVisible = true
+                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_gray_200));
+            }
+            ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE -> {
+                holder.unreadImageView.isVisible = true
+                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_vermilion));
+            }
+            else                                            -> {
+                holder.unreadImageView.isVisible = false
+            }
+        }
     }
 
     class Holder : VectorEpoxyHolder() {
@@ -83,7 +102,6 @@ abstract class ThreadListModel : VectorEpoxyModel() {
         val lastMessageCounterTextView by bind(R.id.messageThreadSummaryCounterTextView)
         val lastMessageTextView by bind(R.id.messageThreadSummaryInfoTextView)
         val unreadImageView by bind(R.id.threadSummaryUnreadImageView)
-
         val rootView by bind(R.id.threadSummaryRootConstraintLayout)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 6e07f0a95f..26970e16b3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -22,6 +22,7 @@ import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.threads.list.model.threadList
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.util.toMatrixItem
 import javax.inject.Inject
 
@@ -44,6 +45,15 @@ class ThreadListController @Inject constructor(
         val host = this
 
         safeViewState.rootThreadEventList.invoke()
+                ?.filter {
+                    if (safeViewState.shouldFilterThreads) {
+                        it.isParticipating
+                    } else {
+                        true
+                    }
+                }?.map {
+                    it.timelineEvent
+                }
                 ?.forEach { timelineEvent ->
                     val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
                     threadList {
@@ -53,7 +63,7 @@ class ThreadListController @Inject constructor(
                         title(timelineEvent.senderInfo.displayName)
                         date(date)
                         rootMessageDeleted(timelineEvent.root.isRedacted())
-                        unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false)
+                        threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
                         rootMessage(timelineEvent.root.getDecryptedTextSummary())
                         lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
                         lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index 715478cec3..25dc14cec6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -29,6 +29,7 @@ import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
 import org.matrix.android.sdk.flow.flow
 
 class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState,
@@ -52,28 +53,29 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
     }
 
     init {
-        observeThreadsList(initialState.shouldFilterThreads)
+        observeThreadsList()
     }
 
     override fun handle(action: EmptyAction) {}
 
-    private fun observeThreadsList(shouldFilterThreads: Boolean) =
-            room?.flow()
-                    ?.liveThreadList()
-                    ?.map {
-                        if (!shouldFilterThreads) return@map it
-                        it.filter { timelineEvent ->
-                            room.isUserParticipatingInThread(timelineEvent.eventId, session.myUserId)
-                        }
-                    }
-                    ?.flowOn(room.coroutineDispatchers.io)
-                    ?.execute { asyncThreads ->
-                        copy(
-                                rootThreadEventList = asyncThreads,
-                                shouldFilterThreads = shouldFilterThreads)
+    private fun observeThreadsList() {
+        room?.flow()
+                ?.liveThreadList()
+                ?.map {
+                    it.map { timelineEvent ->
+                        val isParticipating = room.isUserParticipatingInThread(timelineEvent.eventId)
+                        ThreadTimelineEvent(timelineEvent, isParticipating)
                     }
+                }
+                ?.flowOn(room.coroutineDispatchers.io)
+                ?.execute { asyncThreads ->
+                    copy(rootThreadEventList = asyncThreads)
+                }
+    }
 
     fun applyFiltering(shouldFilterThreads: Boolean) {
-        observeThreadsList(shouldFilterThreads)
+        setState {
+            copy(shouldFilterThreads = shouldFilterThreads)
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
index 01a5239aac..53d2a45344 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
@@ -20,10 +20,10 @@ import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.Uninitialized
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
 
 data class ThreadListViewState(
-        val rootThreadEventList: Async> = Uninitialized,
+        val rootThreadEventList: Async> = Uninitialized,
         val shouldFilterThreads: Boolean = false,
         val roomId: String
 ) : MavericksState{

From 5ceed4096ef7f0dc30bf0fb4910169a7b7bccc6e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 14 Dec 2021 15:44:38 +0200
Subject: [PATCH 026/130] Fix threads sort order, newest first

---
 .../sdk/internal/database/helper/ThreadEventsHelper.kt      | 6 ++++--
 .../sdk/internal/session/room/timeline/DefaultTimeline.kt   | 1 +
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 32184c0ae9..43f6c54c7e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -103,7 +103,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
         TimelineEventEntity
                 .whereRoomId(realm, roomId = roomId)
                 .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
-                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+                .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.DISPLAY_INDEX}", Sort.DESCENDING)
 
 /**
  * Find the number of all the local notifications for the specified room
@@ -188,7 +188,9 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
 
     val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
 
-    if (readReceiptChunkPosition != -1 && readReceiptChunkPosition != readReceiptChunkTimelineEvents.lastIndex) {
+    if(readReceiptChunkPosition == -1) return
+
+    if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
         // If the read receipt is found inside the chunk
 
         val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 707fe487ba..f3ab1930ff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -167,6 +167,7 @@ internal class DefaultTimeline(
                 timelineEvents = rootThreadEventId?.let {
                     TimelineEventEntity
                             .whereRoomId(realm, roomId = roomId)
+                            .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
                             .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
                             .or()
                             .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, it)

From 2aa24f0a0d81f524d04e149229afcfade5bdc132 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 14 Dec 2021 16:30:59 +0200
Subject: [PATCH 027/130] Fix threads sort order, newest first

---
 .../matrix/android/sdk/api/session/threads/ThreadDetails.kt   | 1 +
 .../android/sdk/internal/database/mapper/EventMapper.kt       | 4 +++-
 .../home/room/threads/list/viewmodel/ThreadListController.kt  | 2 +-
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
index ad6e139d01..26e8688d34 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -27,5 +27,6 @@ data class ThreadDetails(
         val numberOfThreads: Int = 0,
         val threadSummarySenderInfo: SenderInfo? = null,
         val threadSummaryLatestTextMessage: String? = null,
+        val lastMessageTimestamp: Long? = null,
         var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
 )
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 53925b1a8f..05070efe1f 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
@@ -113,7 +113,9 @@ internal object EventMapper {
                         )
                     },
                     threadNotificationState = eventEntity.threadNotificationState,
-                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
+                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty(),
+                    lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
+
             )
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 26970e16b3..3f69701a31 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -55,7 +55,7 @@ class ThreadListController @Inject constructor(
                     it.timelineEvent
                 }
                 ?.forEach { timelineEvent ->
-                    val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
+                    val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST)
                     threadList {
                         id(timelineEvent.eventId)
                         avatarRenderer(host.avatarRenderer)

From 6a33c4109185bf7026a68fd5469d4923b0423c6e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 14 Dec 2021 17:45:07 +0200
Subject: [PATCH 028/130] Fix stickers in unencrypted rooms

---
 .../android/sdk/api/session/events/model/Event.kt    | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

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 621e525bd3..d38e861ac3 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
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 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
@@ -336,10 +337,19 @@ fun Event.isAttachmentMessage(): Boolean {
 }
 
 fun Event.getRelationContent(): RelationDefaultContent? {
+    if(eventId?.contains("MgPN5Bqb") == true)
+        Timber.i(":D")
     return if (isEncrypted()) {
         content.toModel()?.relatesTo
     } else {
-        content.toModel()?.relatesTo
+            content.toModel()?.relatesTo ?: run{
+                // Special case to handle stickers, while there is only a local msgtype for stickers
+                if (getClearType() == EventType.STICKER) {
+                    getClearContent().toModel()?.relatesTo
+                } else{
+                    null
+                }
+            }
     }
 }
 

From 20357ce5c4ede9762206f370340821fb1e00539e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 15 Dec 2021 14:38:08 +0200
Subject: [PATCH 029/130]  - Fix remaining conflicts with develop  - Disable
 thread awareness when threads are enabled

---
 .../sdk/api/session/events/model/Event.kt     | 21 +++---
 .../session/room/timeline/TimelineEvent.kt    |  9 +++
 .../room/relation/DefaultRelationService.kt   | 12 ++--
 .../session/room/timeline/DefaultTimeline.kt  |  5 +-
 .../room/timeline/TimelineEventDecryptor.kt   | 17 +++--
 .../session/sync/SyncResponseHandler.kt       |  5 +-
 .../sync/handler/room/RoomSyncHandler.kt      | 11 ++--
 .../home/room/detail/TimelineFragment.kt      | 27 ++++----
 .../detail/composer/MessageComposerAction.kt  |  2 +-
 .../composer/MessageComposerViewModel.kt      | 14 ++--
 .../action/MessageActionsViewModel.kt         | 28 ++++----
 .../timeline/factory/MessageItemFactory.kt    | 12 ++++
 .../home/room/threads/ThreadsActivity.kt      | 18 ++----
 .../list/views/ThreadListBottomSheet.kt       |  2 +-
 .../features/permalink/PermalinkHandler.kt    | 64 ++++++++-----------
 .../room/RequireActiveMembershipViewState.kt  |  4 +-
 16 files changed, 140 insertions(+), 111 deletions(-)

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 8b9d4d0f02..3e0aed8738 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
@@ -36,7 +36,6 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
-import org.matrix.android.sdk.internal.session.room.send.removeInReplyFallbacks
 import timber.log.Timber
 
 typealias Content = JsonDict
@@ -338,20 +337,22 @@ fun Event.isAttachmentMessage(): Boolean {
             }
 }
 
+fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START ||  getClearType() == EventType.POLL_END
+
+fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
+
 fun Event.getRelationContent(): RelationDefaultContent? {
-    if(eventId?.contains("MgPN5Bqb") == true)
-        Timber.i(":D")
     return if (isEncrypted()) {
         content.toModel()?.relatesTo
     } else {
-            content.toModel()?.relatesTo ?: run{
-                // Special case to handle stickers, while there is only a local msgtype for stickers
-                if (getClearType() == EventType.STICKER) {
-                    getClearContent().toModel()?.relatesTo
-                } else{
-                    null
-                }
+        content.toModel()?.relatesTo ?: run {
+            // Special case to handle stickers, while there is only a local msgtype for stickers
+            if (getClearType() == EventType.STICKER) {
+                getClearContent().toModel()?.relatesTo
+            } else {
+                null
             }
+        }
     }
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 932439c81c..e181cc964c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -22,7 +22,9 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.events.model.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.isEdition
+import org.matrix.android.sdk.api.session.events.model.isPoll
 import org.matrix.android.sdk.api.session.events.model.isReply
+import org.matrix.android.sdk.api.session.events.model.isSticker
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
@@ -145,6 +147,13 @@ fun TimelineEvent.isEdition(): Boolean {
     return root.isEdition()
 }
 
+fun TimelineEvent.isPoll(): Boolean =
+        root.isPoll()
+
+fun TimelineEvent.isSticker(): Boolean {
+    return root.isSticker()
+}
+
 /**
  * Get the latest message body, after a possible edition, stripping the reply prefix if necessary
  */
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 4500c71e59..a82d6e8d19 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
@@ -177,9 +177,9 @@ internal class DefaultRelationService @AssistedInject constructor(
                     replyText = replyInThreadText,
                     autoMarkdown = autoMarkdown,
                     rootThreadEventId = rootThreadEventId)
-//                    ?.also {
-//                        saveLocalEcho(it)
-//                    }
+                    ?.also {
+                        saveLocalEcho(it)
+                    }
                     ?: return null
         } else {
             eventFactory.createThreadTextEvent(
@@ -189,9 +189,9 @@ internal class DefaultRelationService @AssistedInject constructor(
                     msgType = msgType,
                     autoMarkdown = autoMarkdown,
                     formattedText = formattedText)
-//                    .also {
-//                        saveLocalEcho(it)
-//                    }
+                    .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/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 4255780999..00388dc5fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -24,6 +24,7 @@ import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.Sort
 import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
@@ -639,7 +640,9 @@ internal class DefaultTimeline(
                 }.map {
                     EventMapper.map(it)
                 }
-        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
+        if(!BuildConfig.THREADING_ENABLED) {
+            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
+        }
     }
 
     private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index 75d02dfd98..aa792f6b9b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.Realm
 import io.realm.RealmConfiguration
+import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -114,11 +115,17 @@ internal class TimelineEventDecryptor @Inject constructor(
                         .findFirst()
 
                 eventEntity?.apply {
-                    val decryptedPayload = threadsAwarenessHandler.handleIfNeededDuringDecryption(
-                            it,
-                            roomId = event.roomId,
-                            event,
-                            result)
+
+                    val decryptedPayload =
+                            if (!BuildConfig.THREADING_ENABLED) {
+                                threadsAwarenessHandler.handleIfNeededDuringDecryption(
+                                        it,
+                                        roomId = event.roomId,
+                                        event,
+                                        result)
+                            } else {
+                                null
+                            }
                     setDecryptionResult(result, decryptedPayload)
                 }
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index f178074507..5ac3eadb75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync
 
 import androidx.work.ExistingPeriodicWorkPolicy
 import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.pushrules.PushRuleService
 import org.matrix.android.sdk.api.pushrules.RuleScope
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
@@ -101,7 +102,9 @@ internal class SyncResponseHandler @Inject constructor(
         val aggregator = SyncResponsePostTreatmentAggregator()
 
         // Prerequisite for thread events handling in RoomSyncHandler
-        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+        if(!BuildConfig.THREADING_ENABLED) {
+            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+        }
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
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 9c3ce66b35..f354a98f80 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import org.matrix.android.sdk.BuildConfig
 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
@@ -375,10 +376,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 decryptIfNeeded(event, roomId)
             }
 
-            threadsAwarenessHandler.handleIfNeeded(
-                    realm = realm,
-                    roomId = roomId,
-                    event = event)
+            if(!BuildConfig.THREADING_ENABLED) {
+                threadsAwarenessHandler.handleIfNeeded(
+                        realm = realm,
+                        roomId = roomId,
+                        event = event)
+            }
 
             val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index ead32a84f7..69189131fc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -721,7 +721,7 @@ class TimelineFragment @Inject constructor(
             }
 
             override fun onVoiceRecordingCancelled() {
-                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
+                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
                 vibrate(requireContext())
                 updateRecordingUiState(RecordingUiState.Idle)
             }
@@ -737,12 +737,12 @@ class TimelineFragment @Inject constructor(
             }
 
             override fun onSendVoiceMessage() {
-                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
+                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId()))
                 updateRecordingUiState(RecordingUiState.Idle)
             }
 
             override fun onDeleteVoiceMessage() {
-                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
+                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
                 updateRecordingUiState(RecordingUiState.Idle)
             }
 
@@ -1388,6 +1388,7 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun updateJumpToReadMarkerViewVisibility() {
+        if(isThreadTimeLine()) return
         viewLifecycleOwner.lifecycleScope.launchWhenResumed {
             withState(roomDetailViewModel) {
                 val showJumpToUnreadBanner = when (it.unreadState) {
@@ -1606,28 +1607,28 @@ class TimelineFragment @Inject constructor(
 
     private fun renderSendMessageResult(sendMessageResult: MessageComposerViewEvents.SendMessageResult) {
         when (sendMessageResult) {
-            is MessageComposerViewEvents.SlashCommandLoading        -> {
+            is MessageComposerViewEvents.SlashCommandLoading               -> {
                 showLoading(null)
             }
-            is MessageComposerViewEvents.SlashCommandError          -> {
+            is MessageComposerViewEvents.SlashCommandError                 -> {
                 displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
             }
-            is MessageComposerViewEvents.SlashCommandUnknown        -> {
+            is MessageComposerViewEvents.SlashCommandUnknown               -> {
                 displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
             }
-            is MessageComposerViewEvents.SlashCommandResultOk       -> {
+            is MessageComposerViewEvents.SlashCommandResultOk              -> {
                 dismissLoadingDialog()
                 views.composerLayout.setTextIfDifferent("")
                 sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
             }
-            is MessageComposerViewEvents.SlashCommandResultError    -> {
+            is MessageComposerViewEvents.SlashCommandResultError           -> {
                 dismissLoadingDialog()
                 displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
             }
-            is MessageComposerViewEvents.SlashCommandNotImplemented -> {
+            is MessageComposerViewEvents.SlashCommandNotImplemented        -> {
                 displayCommandError(getString(R.string.not_implemented))
             }
-            is TextComposerViewEvents.SlashCommandNotSupportedInThreads -> {
+            is MessageComposerViewEvents.SlashCommandNotSupportedInThreads -> {
                 displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command))
             }
         } // .exhaustive
@@ -2145,14 +2146,14 @@ class TimelineFragment @Inject constructor(
                 }
             }
             is EventSharedAction.ReplyInThread              -> {
-                if (!views.voiceMessageRecorderView.isActive()) {
+                if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
                     navigateToThreadTimeline(action.eventId)
                 } else {
                     requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
                 }
             }
             is EventSharedAction.ViewInRoom                 -> {
-                if (!views.voiceMessageRecorderView.isActive()) {
+                if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
                     handleViewInRoomAction()
                 } else {
                     requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
@@ -2386,7 +2387,7 @@ class TimelineFragment @Inject constructor(
             AttachmentTypeSelectorView.Type.AUDIO   -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher)
             AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
             AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
-            AttachmentTypeSelectorView.Type.POLL    -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId)
+            AttachmentTypeSelectorView.Type.POLL    -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId)
         }.exhaustive
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
index 690f127cbd..25c8b17206 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
@@ -35,7 +35,7 @@ sealed class MessageComposerAction : VectorViewModelAction {
     data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
     data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
     object StartRecordingVoiceMessage : MessageComposerAction()
-    data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
+    data class EndRecordingVoiceMessage(val isCancelled: Boolean,val rootThreadEventId: String?) : MessageComposerAction()
     object PauseRecordingVoiceMessage : MessageComposerAction()
     data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
     object PlayOrPauseRecordingPlayback : MessageComposerAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index b02677cecd..4755bddffe 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -93,7 +93,7 @@ class MessageComposerViewModel @AssistedInject constructor(
             is MessageComposerAction.OnTextChanged                  -> handleOnTextChanged(action)
             is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
             is MessageComposerAction.StartRecordingVoiceMessage     -> handleStartRecordingVoiceMessage()
-            is MessageComposerAction.EndRecordingVoiceMessage       -> handleEndRecordingVoiceMessage(action.isCancelled)
+            is MessageComposerAction.EndRecordingVoiceMessage       -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId)
             is MessageComposerAction.PlayOrPauseVoicePlayback       -> handlePlayOrPauseVoicePlayback(action)
             MessageComposerAction.PauseRecordingVoiceMessage        -> handlePauseRecordingVoiceMessage()
             MessageComposerAction.PlayOrPauseRecordingPlayback      -> handlePlayOrPauseRecordingPlayback()
@@ -188,7 +188,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
                         }
                         is ParsedCommand.ErrorCommandNotSupportedInThreads -> {
-                            _viewEvents.post(TextComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.slashCommand))
+                            _viewEvents.post(MessageComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.slashCommand))
                         }
                         is ParsedCommand.SendPlainText                     -> {
                             // Send the text message to the room, without markdown
@@ -491,7 +491,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 eventReplied = timelineEvent)
                     } ?: room.replyToMessage(timelineEvent, action.text.toString(), action.autoMarkdown)
 
-                    _viewEvents.post(TextComposerViewEvents.MessageSent)
+                    _viewEvents.post(MessageComposerViewEvents.MessageSent)
                     popDraft()
                 }
                 is SendMode.Voice   -> {
@@ -774,14 +774,18 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
+    private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
         voiceMessageHelper.stopPlayback()
         if (isCancelled) {
             voiceMessageHelper.deleteRecording()
         } else {
             voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
                 if (audioType.duration > 1000) {
-                    room.sendMedia(audioType.toContentAttachmentData(isVoiceMessage = true), false, emptySet())
+                    room.sendMedia(
+                            attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
+                            compressBeforeSending = false,
+                            roomIds = emptySet(),
+                            rootThreadEventId = rootThreadEventId)
                 } else {
                     voiceMessageHelper.deleteRecording()
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 1368adc3c0..fe615d8c01 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -60,6 +60,8 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 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.hasBeenEdited
+import org.matrix.android.sdk.api.session.room.timeline.isPoll
+import org.matrix.android.sdk.api.session.room.timeline.isSticker
 import org.matrix.android.sdk.flow.flow
 import org.matrix.android.sdk.flow.unwrap
 
@@ -442,14 +444,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
      * Determine whether or not the Reply In Thread bottom sheet setting will be visible
      * to the user
      */
-    // TODO handle reply in thread for images etc
     private fun canReplyInThread(event: TimelineEvent,
                                  messageContent: MessageContent?,
                                  actionPermissions: ActionPermissions): Boolean {
         // Only event of type EventType.MESSAGE are supported for the moment
         if (!BuildConfig.THREADING_ENABLED) return false
         if (initialState.isFromThreadTimeline) return false
-        if (event.root.getClearType() != EventType.MESSAGE) return false
+        if (event.root.getClearType() != EventType.MESSAGE &&
+                !event.isSticker() && !event.isPoll()) return false
         if (!actionPermissions.canSendMessage) return false
         return when (messageContent?.msgType) {
             MessageType.MSGTYPE_TEXT,
@@ -458,8 +460,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             MessageType.MSGTYPE_IMAGE,
             MessageType.MSGTYPE_VIDEO,
             MessageType.MSGTYPE_AUDIO,
-            MessageType.MSGTYPE_FILE -> true
-            else                     -> false
+            MessageType.MSGTYPE_FILE,
+            MessageType.MSGTYPE_POLL_START,
+            MessageType.MSGTYPE_STICKER_LOCAL -> true
+            else                              -> false
         }
     }
 
@@ -468,12 +472,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
      * a thread timeline
      */
     private fun canViewInRoom(event: TimelineEvent,
-                                 messageContent: MessageContent?,
-                                 actionPermissions: ActionPermissions): Boolean {
+                              messageContent: MessageContent?,
+                              actionPermissions: ActionPermissions): Boolean {
         // Only event of type EventType.MESSAGE are supported for the moment
         if (!BuildConfig.THREADING_ENABLED) return false
-        if (!initialState.isFromThreadTimeline) return  false
-        if (event.root.getClearType() != EventType.MESSAGE) return false
+        if (!initialState.isFromThreadTimeline) return false
+        if (event.root.getClearType() != EventType.MESSAGE &&
+                !event.isSticker() && !event.isPoll()) return false
         if (!actionPermissions.canSendMessage) return false
 
         return when (messageContent?.msgType) {
@@ -483,12 +488,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             MessageType.MSGTYPE_IMAGE,
             MessageType.MSGTYPE_VIDEO,
             MessageType.MSGTYPE_AUDIO,
-            MessageType.MSGTYPE_FILE -> event.root.threadDetails?.isRootThread ?: false
-            else                     -> false
+            MessageType.MSGTYPE_FILE,
+            MessageType.MSGTYPE_POLL_START,
+            MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false
+            else                              -> false
         }
     }
 
-
     private fun canQuote(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
         // Only event of type EventType.MESSAGE are supported for the moment
         if (event.root.getClearType() != EventType.MESSAGE) return false
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 a2c38030eb..82aa7094de 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
@@ -226,6 +226,18 @@ class MessageItemFactory @Inject constructor(
             )
         }
 
+        return PollItem_()
+                .attributes(attributes)
+                .eventId(informationData.eventId)
+                .pollQuestion(pollContent.pollCreationInfo?.question?.question ?: "")
+                .pollSent(isPollSent)
+                .totalVotesText(totalVotesText)
+                .optionViewStates(optionViewStates)
+                .highlighted(highlight)
+                .leftGuideline(avatarSizeProvider.leftGuideline)
+                .callback(callback)
+    }
+
     private fun buildAudioMessageItem(messageContent: MessageAudioContent,
                                       @Suppress("UNUSED_PARAMETER")
                                       informationData: MessageInformationData,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index fb1a6006c4..ecbea4cdaf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -19,15 +19,10 @@ package im.vector.app.features.home.room.threads
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.ViewCompat
-import androidx.core.view.children
 import androidx.fragment.app.FragmentTransaction
 import com.google.android.material.appbar.MaterialToolbar
+import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
-import im.vector.app.core.di.ScreenComponent
-import im.vector.app.core.extensions.addFragment
 import im.vector.app.core.extensions.addFragmentToBackstack
 import im.vector.app.core.extensions.replaceFragment
 import im.vector.app.core.platform.ToolbarConfigurable
@@ -42,6 +37,7 @@ import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import javax.inject.Inject
 
+@AndroidEntryPoint
 class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable {
 
     @Inject
@@ -56,10 +52,6 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
 
     override fun getCoordinatorLayout() = views.coordinatorLayout
 
-    override fun injectWith(injector: ScreenComponent) {
-        injector.inject(this)
-    }
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         initFragment()
@@ -83,14 +75,14 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
 
     private fun initThreadListFragment(threadListArgs: ThreadListArgs) {
         replaceFragment(
-                R.id.threadsActivityFragmentContainer,
+                views.threadsActivityFragmentContainer,
                 ThreadListFragment::class.java,
                 threadListArgs)
     }
 
     private fun initThreadTimelineFragment(threadTimelineArgs: ThreadTimelineArgs) =
             replaceFragment(
-                    R.id.threadsActivityFragmentContainer,
+                    views.threadsActivityFragmentContainer,
                     TimelineFragment::class.java,
                     TimelineArgs(
                             roomId = threadTimelineArgs.roomId,
@@ -113,7 +105,7 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
             it.setCustomAnimations(R.anim.animation_slide_in_right, R.anim.animation_slide_out_left, R.anim.animation_slide_in_left, R.anim.animation_slide_out_right)
         }
         addFragmentToBackstack(
-                frameId = R.id.threadsActivityFragmentContainer,
+                container = views.threadsActivityFragmentContainer,
                 fragmentClass = TimelineFragment::class.java,
                 params = TimelineArgs(
                         roomId = timelineEvent.roomId,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
index a4f40a820a..bd62f65897 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
@@ -39,7 +39,7 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment
                     val room = roomId?.let { session?.getRoom(it) }
-                    // Root thread will be opened in timeline
-//                    if(room?.getTimeLineEvent(eventId)?.root?.threadDetails?.isRootThread == true){
-//                        room.getTimeLineEvent(eventId)?.root?.eventId
-//                    }else{
                     room?.getTimeLineEvent(eventId)?.root?.getRootThreadEventId()
-//                    }
-
-                }
-                // MERGE FROM DEVELOP CONFLICT A.K.
-//                openRoom(
-//                        navigationInterceptor,
-//                        context = context,
-//                        roomId = roomId,
-//                        permalinkData = permalinkData,
-//                        rawLink = rawLink,
-//                        buildTask = buildTask
-//                )
-                if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId, rawLink,rootThreadEventId) != true) {
-                    openRoom(
-                            context = context,
-                            roomId = roomId,
-                            permalinkData = permalinkData,
-                            rawLink = rawLink,
-                            buildTask = buildTask,
-                            rootThreadEventId = rootThreadEventId
-                    )
                 }
+                openRoom(
+                        navigationInterceptor,
+                        context = context,
+                        roomId = roomId,
+                        permalinkData = permalinkData,
+                        rawLink = rawLink,
+                        buildTask = buildTask,
+                        rootThreadEventId = rootThreadEventId
+                )
                 true
             }
             is PermalinkData.GroupLink           -> {
@@ -170,14 +155,13 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
      * Open room either joined, or not
      */
     private fun openRoom(
-            // A.K. conflict
-//            navigationInterceptor: NavigationInterceptor?,
+            navigationInterceptor: NavigationInterceptor?,
             context: Context,
             roomId: String?,
             permalinkData: PermalinkData.RoomLink,
             rawLink: Uri,
             buildTask: Boolean,
-            rootThreadEventId: String? =null
+            rootThreadEventId: String? = null
     ) {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         if (roomId == null) {
@@ -194,13 +178,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
             membership?.isActive().orFalse() -> {
                 if (!isSpace && membership == Membership.JOIN) {
                     // If it's a room you're in, let's just open it, you can tap back if needed
-                    // A.K. Conflict
-//                    navigationInterceptor.openJoinedRoomScreen(buildTask, roomId, eventId, rawLink, context)
-                    rootThreadEventId?.let {
-                        val threadTimelineArgs = ThreadTimelineArgs(roomId, displayName = roomSummary.displayName, roomSummary.avatarUrl, it)
-                        navigator.openThread(context, threadTimelineArgs, eventId)
-                    } ?: navigator.openRoom(context, roomId, eventId, buildTask)
-
+                    navigationInterceptor.openJoinedRoomScreen(buildTask, roomId, eventId, rawLink, context, rootThreadEventId, roomSummary)
                 } else {
                     // maybe open space preview navigator.openSpacePreview(context, roomId)? if already joined?
                     navigator.openMatrixToBottomSheet(context, rawLink.toString())
@@ -213,9 +191,19 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
         }
     }
 
-    private fun NavigationInterceptor?.openJoinedRoomScreen(buildTask: Boolean, roomId: String, eventId: String?, rawLink: Uri, context: Context) {
-        if (this?.navToRoom(roomId, eventId, rawLink) != true) {
-            navigator.openRoom(context, roomId, eventId, buildTask)
+    private fun NavigationInterceptor?.openJoinedRoomScreen(buildTask: Boolean,
+                                                            roomId: String,
+                                                            eventId: String?,
+                                                            rawLink: Uri,
+                                                            context: Context,
+                                                            rootThreadEventId: String?,
+                                                            roomSummary: RoomSummary
+    ) {
+        if (this?.navToRoom(roomId, eventId, rawLink, rootThreadEventId) != true) {
+            rootThreadEventId?.let {
+                val threadTimelineArgs = ThreadTimelineArgs(roomId, displayName = roomSummary.displayName, roomSummary.avatarUrl, it)
+                navigator.openThread(context, threadTimelineArgs, eventId)
+            } ?: navigator.openRoom(context, roomId, eventId, buildTask)
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt
index 7a5363100f..7e4af1b7d5 100644
--- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewState.kt
@@ -17,7 +17,7 @@
 package im.vector.app.features.room
 
 import com.airbnb.mvrx.MavericksState
-import im.vector.app.features.home.room.detail.RoomDetailArgs
+import im.vector.app.features.home.room.detail.arguments.TimelineArgs
 import im.vector.app.features.roommemberprofile.RoomMemberProfileArgs
 import im.vector.app.features.roomprofile.RoomProfileArgs
 
@@ -25,7 +25,7 @@ data class RequireActiveMembershipViewState(
         val roomId: String? = null
 ) : MavericksState {
 
-    constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
+    constructor(args: TimelineArgs) : this(roomId = args.roomId)
 
     constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
 

From 3acdccb339e62e800ab995522a99527ac5417a9e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 15 Dec 2021 16:31:58 +0200
Subject: [PATCH 030/130] Disable polls from within threads but allow users to
 vote if the poll is a root thread message

---
 .../org/matrix/android/sdk/api/session/events/model/Event.kt  | 3 ++-
 .../sdk/internal/database/RealmSessionStoreMigration.kt       | 2 +-
 .../vector/app/features/home/room/detail/TimelineFragment.kt  | 4 +++-
 3 files changed, 6 insertions(+), 3 deletions(-)

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 3e0aed8738..5f9a15de02 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
@@ -205,6 +205,7 @@ data class Event(
             isAudioMessage()       -> "sent an audio file."
             isImageMessage()       -> "sent an image."
             isVideoMessage()       -> "sent a video."
+            isPoll()               -> "created a poll."
             else                   -> text
         }
     }
@@ -337,7 +338,7 @@ fun Event.isAttachmentMessage(): Boolean {
             }
 }
 
-fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START ||  getClearType() == EventType.POLL_END
+fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
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 5b9a74fe9e..e7968b8786 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
@@ -55,7 +55,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
 ) : RealmMigration {
 
     companion object {
-        const val SESSION_STORE_SCHEMA_VERSION = 19L
+        const val SESSION_STORE_SCHEMA_VERSION = 20L
     }
 
     /**
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 69189131fc..c89e4cb980 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1448,7 +1448,9 @@ class TimelineFragment @Inject constructor(
             override fun onAddAttachment() {
                 if (!::attachmentTypeSelector.isInitialized) {
                     attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment)
-                    attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.POLL, vectorPreferences.labsEnablePolls())
+                    attachmentTypeSelector.setAttachmentVisibility(
+                            AttachmentTypeSelectorView.Type.POLL,
+                            vectorPreferences.labsEnablePolls() && !isThreadTimeLine())
                 }
                 attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing)
             }

From bc6e89b5030a6084126757580dfc204a58596f53 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 15 Dec 2021 18:49:22 +0200
Subject: [PATCH 031/130] Disable user typing from thread timeline

---
 .../im/vector/app/features/home/room/detail/TimelineFragment.kt  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index c89e4cb980..0bff2da082 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1488,6 +1488,7 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun observerUserTyping() {
+        if(isThreadTimeLine()) return
         views.composerLayout.views.composerEditText.textChanges()
                 .skipInitialValue()
                 .debounce(300)

From 638d56c7075bc8da6ebcbb8851feb1bf10d74107 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 16 Dec 2021 17:10:29 +0200
Subject: [PATCH 032/130] Fix update from develop/prod to threads

---
 .../sdk/internal/database/RealmSessionStoreMigration.kt  | 4 ++++
 .../internal/session/room/timeline/DefaultTimeline.kt    | 9 +++++----
 2 files changed, 9 insertions(+), 4 deletions(-)

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 e7968b8786..04c48a1889 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.api.session.threads.ThreadNotificationState
 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
@@ -403,6 +404,9 @@ internal class RealmSessionStoreMigration @Inject constructor(
                 ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
                 ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
                 ?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
+                ?.transform {
+                    it.setString(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NO_NEW_MESSAGE.name)
+                }
                 ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 00388dc5fa..78ebe82129 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -602,8 +602,11 @@ internal class DefaultTimeline(
             nextDisplayIndex = offsetIndex + 1
         }
 
-        // Prerequisite to in order for the ThreadsAwarenessHandler to work properly
-        fetchRootThreadEventsIfNeeded(offsetResults)
+
+        if(!BuildConfig.THREADING_ENABLED) {
+            // Prerequisite to in order for the ThreadsAwarenessHandler to work properly
+            fetchRootThreadEventsIfNeeded(offsetResults)
+        }
 
         offsetResults.forEach { eventEntity ->
 
@@ -640,9 +643,7 @@ internal class DefaultTimeline(
                 }.map {
                     EventMapper.map(it)
                 }
-        if(!BuildConfig.THREADING_ENABLED) {
             threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
-        }
     }
 
     private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {

From a187e0ec33d1d461e76db977900a8920be967759 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 16 Dec 2021 22:03:42 +0200
Subject: [PATCH 033/130] Enhance thread awareness to recognise the type of
 messages that are not able to be send as a reply such as images, videos,
 audios, stickers

---
 build.gradle                                  |  2 +-
 .../handler/room/ThreadsAwarenessHandler.kt   | 26 ++++++++++++++++---
 2 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/build.gradle b/build.gradle
index f057d234e5..433ca8bc9d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -154,7 +154,7 @@ project(":diff-match-patch") {
 
 // Global configurations across all modules
 ext {
-    isThreadingEnabled = true
+    isThreadingEnabled = false
 }
 
 //project(":matrix-sdk-android") {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index 767a967522..30876c21da 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
 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.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.api.util.JsonDict
@@ -43,6 +44,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
 import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -84,7 +86,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
         if (eventList.isNullOrEmpty()) return
 
         val threadsToFetch = emptyMap().toMutableMap()
-        Realm.getInstance(monarchy.realmConfiguration).use {  realm ->
+        Realm.getInstance(monarchy.realmConfiguration).use { realm ->
             eventList.asSequence()
                     .filter {
                         isThreadEvent(it) && it.roomId != null
@@ -176,11 +178,29 @@ internal class ThreadsAwarenessHandler @Inject constructor(
         if (!isThreadEvent(event)) return null
         val rootThreadEventId = getRootThreadEventId(event) ?: return null
         val payload = decryptedResult?.toMutableMap() ?: return null
-        val body = getValueFromPayload(payload, "body") ?: return null
+        var body = getValueFromPayload(payload, "body") ?: return null
         val msgType = getValueFromPayload(payload, "msgtype") ?: return null
         val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
         val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
 
+        // Check the event type
+        when (msgType) {
+            MessageType.MSGTYPE_STICKER_LOCAL -> {
+                body = "sent a sticker from within a thread"
+            }
+            MessageType.MSGTYPE_FILE     -> {
+                body = "sent a file from within a thread"
+            }
+            MessageType.MSGTYPE_VIDEO    -> {
+                body = "Sent a video from within a thread"
+            }
+            MessageType.MSGTYPE_IMAGE    -> {
+                body = "sent an image from within a thread"
+            }
+            MessageType.MSGTYPE_AUDIO    -> {
+                body = "sent an audio file from within a thread"
+            }
+        }
         decryptIfNeeded(rootThreadEvent, roomId)
 
         val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body")
@@ -197,7 +217,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 body)
 
         val messageTextContent = MessageTextContent(
-                msgType = msgType,
+                msgType = "m.text",
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = body,
                 formattedBody = replyFormatted

From a60f6e996a7b0991ea171df1995b4e2402b4dcdc Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 17 Dec 2021 00:46:47 +0200
Subject: [PATCH 034/130] Enhance thread awareness to support stickers

---
 .../handler/room/ThreadsAwarenessHandler.kt     | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index 30876c21da..eb03875cb1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -21,6 +21,7 @@ import io.realm.Realm
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 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.RelationType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -179,7 +180,13 @@ internal class ThreadsAwarenessHandler @Inject constructor(
         val rootThreadEventId = getRootThreadEventId(event) ?: return null
         val payload = decryptedResult?.toMutableMap() ?: return null
         var body = getValueFromPayload(payload, "body") ?: return null
-        val msgType = getValueFromPayload(payload, "msgtype") ?: return null
+        val msgType = getValueFromPayload(payload, "msgtype") ?: run {
+            if (payload["type"]?.toString() == EventType.STICKER) {
+                MessageType.MSGTYPE_STICKER_LOCAL
+            } else {
+                return null
+            }
+        }
         val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
         val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
 
@@ -188,16 +195,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
             MessageType.MSGTYPE_STICKER_LOCAL -> {
                 body = "sent a sticker from within a thread"
             }
-            MessageType.MSGTYPE_FILE     -> {
+            MessageType.MSGTYPE_FILE          -> {
                 body = "sent a file from within a thread"
             }
-            MessageType.MSGTYPE_VIDEO    -> {
+            MessageType.MSGTYPE_VIDEO         -> {
                 body = "Sent a video from within a thread"
             }
-            MessageType.MSGTYPE_IMAGE    -> {
+            MessageType.MSGTYPE_IMAGE         -> {
                 body = "sent an image from within a thread"
             }
-            MessageType.MSGTYPE_AUDIO    -> {
+            MessageType.MSGTYPE_AUDIO         -> {
                 body = "sent an audio file from within a thread"
             }
         }

From 57234651067ef54e775ca23b395511f9b3cc7b4e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 17 Dec 2021 01:23:09 +0200
Subject: [PATCH 035/130] Fix local notification badge number

---
 build.gradle                                                    | 2 +-
 .../android/sdk/internal/database/helper/ThreadEventsHelper.kt  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 433ca8bc9d..f057d234e5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -154,7 +154,7 @@ project(":diff-match-patch") {
 
 // Global configurations across all modules
 ext {
-    isThreadingEnabled = false
+    isThreadingEnabled = true
 }
 
 //project(":matrix-sdk-android") {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 43f6c54c7e..3610d5871b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -113,9 +113,11 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo
         TimelineEventEntity
                 .whereRoomId(realm, roomId = roomId)
                 .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .beginGroup()
                 .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
                 .or()
                 .equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
+                .endGroup()
 
 /**
  * Returns whether or not the given user is participating in a current thread

From cc7e3ea78cfe68d9676519374e96072e2741e8d8 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 17 Dec 2021 01:25:50 +0200
Subject: [PATCH 036/130] Improve init thread query

---
 .../sdk/internal/session/room/timeline/DefaultTimeline.kt       | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 78ebe82129..1fe05ced68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -173,9 +173,11 @@ internal class DefaultTimeline(
                     TimelineEventEntity
                             .whereRoomId(realm, roomId = roomId)
                             .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
+                            .beginGroup()
                             .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
                             .or()
                             .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, it)
+                            .endGroup()
                             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                             .findAll()
 

From ed48eb38c9dbda84a9929ef67be4127e7a38b9f5 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 21 Dec 2021 13:23:17 +0200
Subject: [PATCH 037/130] Apply ktlinFormat

---
 .../java/org/matrix/android/sdk/flow/FlowRoom.kt   |  1 -
 .../api/session/room/timeline/TimelineService.kt   |  1 -
 .../api/session/threads/ThreadNotificationState.kt |  1 -
 .../database/RealmSessionStoreMigration.kt         |  2 --
 .../internal/database/helper/ThreadEventsHelper.kt |  7 +------
 .../sdk/internal/database/model/ChunkEntity.kt     |  1 -
 .../sdk/internal/database/model/EventEntity.kt     |  1 -
 .../internal/database/query/ChunkEntityQueries.kt  |  4 ----
 .../internal/database/query/EventEntityQueries.kt  |  2 --
 .../database/query/TimelineEventEntityQueries.kt   |  2 --
 .../room/relation/DefaultRelationService.kt        |  1 -
 .../session/room/send/DefaultSendService.kt        |  1 -
 .../session/room/send/LocalEchoEventFactory.kt     |  1 -
 .../session/room/timeline/DefaultTimeline.kt       |  7 +------
 .../session/room/timeline/PaginationTask.kt        |  1 -
 .../room/timeline/TimelineEventDecryptor.kt        |  1 -
 .../room/timeline/TokenChunkEventPersistor.kt      |  1 -
 .../internal/session/sync/SyncResponseHandler.kt   |  2 +-
 .../session/sync/handler/room/RoomSyncHandler.kt   |  2 +-
 .../sync/handler/room/ThreadsAwarenessHandler.kt   |  1 -
 .../command/AutocompleteCommandPresenter.kt        |  6 ++----
 .../vector/app/features/command/CommandParser.kt   |  4 ++--
 .../home/room/detail/RoomDetailActivity.kt         |  2 +-
 .../home/room/detail/RoomDetailViewState.kt        |  1 -
 .../features/home/room/detail/TimelineFragment.kt  | 12 ++----------
 .../room/detail/composer/MessageComposerAction.kt  |  2 +-
 .../detail/composer/MessageComposerViewModel.kt    | 14 ++++++--------
 .../detail/timeline/TimelineEventController.kt     |  4 ++--
 .../helper/TimelineEventVisibilityHelper.kt        |  2 +-
 .../detail/timeline/item/AbsBaseMessageItem.kt     |  1 +
 .../room/detail/timeline/item/AbsMessageItem.kt    |  4 ++--
 .../features/home/room/threads/ThreadsActivity.kt  |  3 +--
 .../room/threads/list/model/ThreadListModel.kt     |  5 ++---
 .../threads/list/viewmodel/ThreadListViewState.kt  |  2 +-
 .../im/vector/app/features/navigation/Navigator.kt |  1 -
 35 files changed, 28 insertions(+), 75 deletions(-)

diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index cdb3bdf9c2..46acdc123b 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.Flow
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.Room
-import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 4ac4aab4e6..bf48353918 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -91,5 +91,4 @@ interface TimelineService {
      * @param rootThreadEventId the eventId of the current thread
      */
     suspend fun markThreadAsRead(rootThreadEventId: String)
-
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
index 093e4a7627..58cc3a0706 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
@@ -30,5 +30,4 @@ enum class ThreadNotificationState {
     // The is at least one new message that should bi highlighted
     // ex. "Hello @aris.kotsomitopoulos"
     NEW_HIGHLIGHTED_MESSAGE;
-
 }
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 04c48a1889..88a7b7abb3 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
@@ -26,7 +26,6 @@ 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.api.session.threads.ThreadNotificationState
-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
@@ -89,7 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion <= 17) migrateTo18(realm)
         if (oldVersion <= 18) migrateTo19(realm)
         if (oldVersion <= 19) migrateTo20(realm)
-
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 3610d5871b..557bb4bdf1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -37,13 +37,10 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId
  * of threads included. If there is no root thread event no action is done
  */
 internal fun Map.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) {
-
     if (!BuildConfig.THREADING_ENABLED) return
 
     for ((rootThreadEventId, eventEntity) in this) {
-
         eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let {
-
             if (it.isNullOrEmpty()) return@let
 
             val latestMessage = it.firstOrNull()
@@ -55,7 +52,6 @@ internal fun Map.updateThreadSummaryIfNeeded(roomId: String
                     threadsCounted = it.size,
                     latestMessageTimelineEventEntity = latestMessage
             )
-
         }
     }
 
@@ -175,7 +171,6 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
  * immediately so we should not display wrong notifications
  */
 internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
-
     val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
 
     val readReceiptChunk = ChunkEntity
@@ -190,7 +185,7 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
 
     val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
 
-    if(readReceiptChunkPosition == -1) return
+    if (readReceiptChunkPosition == -1) return
 
     if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
         // If the read receipt is found inside the chunk
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 0b9a1ee8cc..68533a3c19 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
@@ -44,7 +44,6 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
     val room: RealmResults? = null
 
     companion object
-
 }
 
 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 3a7611fc36..f4e12bf3ed 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
@@ -95,5 +95,4 @@ internal open class EventEntity(@Index var eventId: String = "",
     }
 
     fun isThread(): Boolean = 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 2261d9786a..156a8dd767 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
@@ -46,9 +46,6 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room
             .findFirst()
 }
 
-
-
-
 internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults {
     return realm.where()
             .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
@@ -59,7 +56,6 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
     return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
 }
 
-
 internal fun ChunkEntity.Companion.create(
         realm: Realm,
         prevToken: String?,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
index a439d6aae7..f7fa1037ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
@@ -49,13 +49,11 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
             .equalTo(EventEntityFields.EVENT_ID, eventId)
 }
 
-
 internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery {
     return realm.where()
             .equalTo(EventEntityFields.ROOM_ID, roomId)
 }
 
-
 internal fun EventEntity.Companion.where(realm: Realm, eventIds: List): RealmQuery {
     return realm.where()
             .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 9ce59904b4..63f41ebf2c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -25,11 +25,9 @@ import io.realm.kotlin.where
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
-import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
-import timber.log.Timber
 
 internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery {
     return realm.where()
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 a82d6e8d19..d459e79a4a 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
@@ -169,7 +169,6 @@ internal class DefaultRelationService @AssistedInject constructor(
             autoMarkdown: Boolean,
             formattedText: String?,
             eventReplied: TimelineEvent?): Cancelable? {
-
         val event = if (eventReplied != null) {
             eventFactory.createReplyTextEvent(
                     roomId = roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index e89ea77835..8fe799b1a1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -258,7 +258,6 @@ internal class DefaultSendService @AssistedInject constructor(
                            roomIds: Set,
                            rootThreadEventId: String?
     ): Cancelable {
-
         // Ensure that the event will not be send in a thread if we are a different flow.
         // Like sending files to multiple rooms
         val rootThreadId = if (roomIds.isNotEmpty()) null else rootThreadEventId
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 27e50e5e93..1046bcee49 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
@@ -397,7 +397,6 @@ internal class LocalEchoEventFactory @Inject constructor(
             msgType: String,
             autoMarkdown: Boolean,
             formattedText: String?): Event {
-
         val content = formattedText?.let { TextContent(text, it) } ?: createTextContent(text, autoMarkdown)
         return createEvent(
                 roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 1fe05ced68..69e56a85d0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -180,7 +180,6 @@ internal class DefaultTimeline(
                             .endGroup()
                             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                             .findAll()
-
                 } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
 
                 timelineEvents.addChangeListener(eventsChangeListener)
@@ -332,7 +331,6 @@ internal class DefaultTimeline(
         val firstCacheEvent = results.firstOrNull()
         val chunkEntity = getLiveChunk()
 
-
         updateState(Timeline.Direction.FORWARDS) {
             it.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),   // what is in DB
@@ -340,7 +338,6 @@ internal class DefaultTimeline(
             )
         }
         updateState(Timeline.Direction.BACKWARDS) {
-
             it.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
                     hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
@@ -497,7 +494,6 @@ internal class DefaultTimeline(
      * This has to be called on TimelineThread as it accesses realm live results
      */
     private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
-
         val currentChunk = getLiveChunk()
         val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
         if (token == null) {
@@ -604,8 +600,7 @@ internal class DefaultTimeline(
             nextDisplayIndex = offsetIndex + 1
         }
 
-
-        if(!BuildConfig.THREADING_ENABLED) {
+        if (!BuildConfig.THREADING_ENABLED) {
             // Prerequisite to in order for the ThreadsAwarenessHandler to work properly
             fetchRootThreadEventsIfNeeded(offsetResults)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
index cb23061eda..8aeccb66c8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
@@ -21,7 +21,6 @@ import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.filter.FilterRepository
 import org.matrix.android.sdk.internal.session.room.RoomAPI
 import org.matrix.android.sdk.internal.task.Task
-import timber.log.Timber
 import javax.inject.Inject
 
 internal interface PaginationTask : Task {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index aa792f6b9b..a4d48903ad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -115,7 +115,6 @@ internal class TimelineEventDecryptor @Inject constructor(
                         .findFirst()
 
                 eventEntity?.apply {
-
                     val decryptedPayload =
                             if (!BuildConfig.THREADING_ENABLED) {
                                 threadsAwarenessHandler.handleIfNeededDuringDecryption(
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 8331d3f5f4..b909b2feef 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
@@ -238,7 +238,6 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 // This is a normal event or a root thread one
                 optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
             }
-
         }
 
         // Find all the chunks which contain at least one event from the list of eventIds
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 5ac3eadb75..a3cfddd472 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -102,7 +102,7 @@ internal class SyncResponseHandler @Inject constructor(
         val aggregator = SyncResponsePostTreatmentAggregator()
 
         // Prerequisite for thread events handling in RoomSyncHandler
-        if(!BuildConfig.THREADING_ENABLED) {
+        if (!BuildConfig.THREADING_ENABLED) {
             threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
         }
 
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 f354a98f80..8386071755 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
@@ -376,7 +376,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 decryptIfNeeded(event, roomId)
             }
 
-            if(!BuildConfig.THREADING_ENABLED) {
+            if (!BuildConfig.THREADING_ENABLED) {
                 threadsAwarenessHandler.handleIfNeeded(
                         realm = realm,
                         roomId = roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index eb03875cb1..a4ebfabc5c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -45,7 +45,6 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
 import org.matrix.android.sdk.internal.util.awaitTransaction
-import timber.log.Timber
 import javax.inject.Inject
 
 /**
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
index 7846ebab37..7afe3eaebc 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
@@ -25,10 +25,7 @@ import im.vector.app.BuildConfig
 import im.vector.app.features.autocomplete.AutocompleteClickListener
 import im.vector.app.features.autocomplete.RecyclerViewPresenter
 import im.vector.app.features.command.Command
-import im.vector.app.features.home.room.detail.AutoCompleter
 import im.vector.app.features.settings.VectorPreferences
-import timber.log.Timber
-import javax.inject.Inject
 
 class AutocompleteCommandPresenter @AssistedInject constructor(
         @Assisted val isInThreadTimeline: Boolean,
@@ -62,8 +59,9 @@ class AutocompleteCommandPresenter @AssistedInject constructor(
                 .filter {
                     if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
                         it.isThreadCommand
-                    } else
+                    } else {
                         true
+                    }
                 }
                 .filter {
                     if (query.isNullOrEmpty()) {
diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
index 3eb01758f8..7ff2223682 100644
--- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
@@ -64,14 +64,14 @@ object CommandParser {
 
             // If the command is not supported by threads return error
 
-            if(BuildConfig.THREADING_ENABLED && isInThreadTimeline){
+            if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
                 val slashCommand = messageParts.first()
                 val notSupportedCommandsInThreads = Command.values().filter {
                     !it.isThreadCommand
                 }.map {
                     it.command
                 }
-                if(notSupportedCommandsInThreads.contains(slashCommand)){
+                if (notSupportedCommandsInThreads.contains(slashCommand)) {
                     return ParsedCommand.ErrorCommandNotSupportedInThreads(slashCommand)
                 }
             }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
index 40af675e66..bc1e17f984 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
@@ -37,8 +37,8 @@ import im.vector.app.core.platform.ToolbarConfigurable
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.databinding.ActivityRoomDetailBinding
 import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
-import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
 import im.vector.app.features.home.room.detail.arguments.TimelineArgs
+import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
 import im.vector.app.features.matrixto.MatrixToBottomSheet
 import im.vector.app.features.navigation.Navigator
 import im.vector.app.features.room.RequireActiveMembershipAction
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 051c9b6500..f41ac504fc 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
@@ -89,5 +89,4 @@ data class RoomDetailViewState(
     fun isDm() = asyncRoomSummary()?.isDirect == true
 
     fun isThreadTimeline() = rootThreadEventId != null
-
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 0bff2da082..7778294aa3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -63,8 +63,6 @@ import com.airbnb.epoxy.EpoxyModel
 import com.airbnb.epoxy.OnModelBuildFinishedListener
 import com.airbnb.epoxy.addGlidePreloader
 import com.airbnb.epoxy.glidePreloader
-import com.airbnb.mvrx.Mavericks
-import com.airbnb.mvrx.activityViewModel
 import com.airbnb.mvrx.args
 import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
@@ -136,7 +134,6 @@ import im.vector.app.features.command.Command
 import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
 import im.vector.app.features.crypto.verification.VerificationBottomSheet
 import im.vector.app.features.home.AvatarRenderer
-import im.vector.app.features.home.UnreadMessagesSharedViewModel
 import im.vector.app.features.home.room.detail.arguments.TimelineArgs
 import im.vector.app.features.home.room.detail.composer.MessageComposerAction
 import im.vector.app.features.home.room.detail.composer.MessageComposerView
@@ -975,7 +972,6 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onPrepareOptionsMenu(menu: Menu) {
-
         menu.forEach {
             it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
         }
@@ -1014,7 +1010,6 @@ class TimelineFragment @Inject constructor(
 
             // Handle custom threads badge notification
             updateMenuThreadNotificationBadge(menu, state)
-
         }
     }
 
@@ -1057,7 +1052,6 @@ class TimelineFragment @Inject constructor(
                     val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, it)
                     copyToClipboard(requireContext(), permalink, false)
                     showSnackWithMessage(getString(R.string.copied_to_clipboard))
-
                 }
                 true
             }
@@ -1109,7 +1103,6 @@ class TimelineFragment @Inject constructor(
                 val int = RoomDetailActivity.newIntent(con, newRoom)
                 int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
                 con.startActivity(int)
-
             }
         }
     }
@@ -1388,7 +1381,7 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun updateJumpToReadMarkerViewVisibility() {
-        if(isThreadTimeLine()) return
+        if (isThreadTimeLine()) return
         viewLifecycleOwner.lifecycleScope.launchWhenResumed {
             withState(roomDetailViewModel) {
                 val showJumpToUnreadBanner = when (it.unreadState) {
@@ -1488,7 +1481,7 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun observerUserTyping() {
-        if(isThreadTimeLine()) return
+        if (isThreadTimeLine()) return
         views.composerLayout.views.composerEditText.textChanges()
                 .skipInitialValue()
                 .debounce(300)
@@ -1774,7 +1767,6 @@ class TimelineFragment @Inject constructor(
                             if (roomId != timelineArgs.roomId) return false
                             // Navigation to same room
                             if (!isThreadTimeLine()) {
-
                                 if (rootThreadEventId != null) {
                                     // Thread link, so PermalinkHandler will handle the navigation
                                     return false
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
index 25c8b17206..10cef39942 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
@@ -35,7 +35,7 @@ sealed class MessageComposerAction : VectorViewModelAction {
     data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
     data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
     object StartRecordingVoiceMessage : MessageComposerAction()
-    data class EndRecordingVoiceMessage(val isCancelled: Boolean,val rootThreadEventId: String?) : MessageComposerAction()
+    data class EndRecordingVoiceMessage(val isCancelled: Boolean, val rootThreadEventId: String?) : MessageComposerAction()
     object PauseRecordingVoiceMessage : MessageComposerAction()
     data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
     object PlayOrPauseRecordingPlayback : MessageComposerAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 4755bddffe..83c2938b45 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -30,7 +30,6 @@ import im.vector.app.features.attachments.toContentAttachmentData
 import im.vector.app.features.command.CommandParser
 import im.vector.app.features.command.ParsedCommand
 import im.vector.app.features.home.room.detail.ChatEffect
-import im.vector.app.features.home.room.detail.TimelineFragment
 import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
 import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
 import im.vector.app.features.home.room.detail.toMessageType
@@ -167,13 +166,14 @@ class MessageComposerViewModel @AssistedInject constructor(
                     when (val slashCommandResult = CommandParser.parseSplashCommand(action.text, state.isInThreadTimeline())) {
                         is ParsedCommand.ErrorNotACommand                  -> {
                             // Send the text message to the room
-                            if (state.rootThreadEventId != null)
+                            if (state.rootThreadEventId != null) {
                                 room.replyInThread(
                                         rootThreadEventId = state.rootThreadEventId,
                                         replyInThreadText = action.text.toString(),
                                         autoMarkdown = action.autoMarkdown)
-                            else
+                            } else {
                                 room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
+                            }
 
                             _viewEvents.post(MessageComposerViewEvents.MessageSent)
                             popDraft()
@@ -192,13 +192,14 @@ class MessageComposerViewModel @AssistedInject constructor(
                         }
                         is ParsedCommand.SendPlainText                     -> {
                             // Send the text message to the room, without markdown
-                            if (state.rootThreadEventId != null)
+                            if (state.rootThreadEventId != null) {
                                 room.replyInThread(
                                         rootThreadEventId = state.rootThreadEventId,
                                         replyInThreadText = action.text.toString(),
                                         autoMarkdown = false)
-                            else
+                            } else {
                                 room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
+                            }
                             _viewEvents.post(MessageComposerViewEvents.MessageSent)
                             popDraft()
                         }
@@ -258,7 +259,6 @@ class MessageComposerViewModel @AssistedInject constructor(
                             popDraft()
                         }
                         is ParsedCommand.SendRainbow                       -> {
-
                             val message = slashCommandResult.message.toString()
                             state.rootThreadEventId?.let {
                                 room.replyInThread(
@@ -283,7 +283,6 @@ class MessageComposerViewModel @AssistedInject constructor(
                             popDraft()
                         }
                         is ParsedCommand.SendSpoiler                       -> {
-
                             val text = "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})"
                             val formattedText = "${slashCommandResult.message}"
                             state.rootThreadEventId?.let {
@@ -299,7 +298,6 @@ class MessageComposerViewModel @AssistedInject constructor(
                             popDraft()
                         }
                         is ParsedCommand.SendShrug                         -> {
-
                             sendPrefixedMessage("¯\\_(ツ)_/¯", slashCommandResult.message, state.rootThreadEventId)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
                             popDraft()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index b091ea2fb7..20a3f34338 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -105,7 +105,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                 rootThreadEventId = state.rootThreadEventId
         )
 
-        fun isFromThreadTimeline():Boolean = rootThreadEventId != null
+        fun isFromThreadTimeline(): Boolean = rootThreadEventId != null
     }
 
     interface Callback :
@@ -200,7 +200,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                 // it's sent by the same user so we are sure we have up to date information.
                 val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
                 val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
-                    timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline()  )
+                    timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline())
                 }
                 if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
                     modelCache[prevDisplayableEventIndex] = null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 7206fa2280..7efefc5209 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -128,7 +128,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
             return true
         }
 
-        if(BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null){
+        if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null) {
             return true
         }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
index a3e808c7bb..080b766258 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
@@ -127,6 +127,7 @@ abstract class AbsBaseMessageItem : BaseEventItem
         val messageColorProvider: MessageColorProvider
         val itemLongClickListener: View.OnLongClickListener?
         val itemClickListener: ClickListener?
+
         //        val memberClickListener: ClickListener?
         val reactionPillCallback: TimelineEventController.ReactionPillCallback?
 
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 a9455a0e3c..f75df30916 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
@@ -124,10 +124,10 @@ abstract class AbsMessageItem : AbsBaseMessageItem
                 val displayName = threadDetails.threadSummarySenderInfo?.displayName
                 val avatarUrl = threadDetails.threadSummarySenderInfo?.avatarUrl
                 attributes.avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView)
-                updateHighlightedMessageHeight(holder,true)
+                updateHighlightedMessageHeight(holder, true)
             } ?: run {
                 holder.threadSummaryConstraintLayout.isVisible = false
-                updateHighlightedMessageHeight(holder,false)
+                updateHighlightedMessageHeight(holder, false)
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index ecbea4cdaf..84d270a2c2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -29,8 +29,8 @@ import im.vector.app.core.platform.ToolbarConfigurable
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.databinding.ActivityThreadsBinding
 import im.vector.app.features.home.AvatarRenderer
-import im.vector.app.features.home.room.detail.arguments.TimelineArgs
 import im.vector.app.features.home.room.detail.TimelineFragment
+import im.vector.app.features.home.room.detail.arguments.TimelineArgs
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
 import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
 import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
@@ -152,7 +152,6 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
                 putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs)
                 putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate)
                 putExtra(THREAD_LIST_ARGS, threadListArgs)
-
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
index 286f027915..b890952719 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
@@ -77,15 +77,14 @@ abstract class ThreadListModel : VectorEpoxyModel() {
     }
 
     private fun renderNotificationState(holder: Holder) {
-
         when (threadNotificationState) {
             ThreadNotificationState.NEW_MESSAGE             -> {
                 holder.unreadImageView.isVisible = true
-                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_gray_200));
+                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_gray_200))
             }
             ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE -> {
                 holder.unreadImageView.isVisible = true
-                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_vermilion));
+                holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_vermilion))
             }
             else                                            -> {
                 holder.unreadImageView.isVisible = false
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
index 53d2a45344..2a70a5be1e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt
@@ -26,7 +26,7 @@ data class ThreadListViewState(
         val rootThreadEventList: Async> = Uninitialized,
         val shouldFilterThreads: Boolean = false,
         val roomId: String
-) : MavericksState{
+) : MavericksState {
 
     constructor(args: ThreadListArgs) : this(roomId = args.roomId)
 }
diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
index b6da13ca33..5254bf4838 100644
--- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt
@@ -154,5 +154,4 @@ interface Navigator {
     fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
 
     fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
-
 }

From 5a7d12a9a5896a7aa18d6c81dc1e865aad37faf5 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 21 Dec 2021 20:04:50 +0200
Subject: [PATCH 038/130] Enhance RoomEventFilter with MSC3440

---
 .../android/sdk/api/session/events/model/Event.kt     |  4 ++--
 .../sdk/api/session/events/model/RelationType.kt      |  4 ++--
 .../sdk/internal/session/filter/RoomEventFilter.kt    | 10 ++++++++++
 .../android/sdk/internal/session/room/RoomAPI.kt      | 11 +++++++++++
 .../session/room/send/LocalEchoEventFactory.kt        | 10 +++++-----
 .../sdk/internal/session/room/send/TextContent.kt     |  2 +-
 .../sync/handler/room/ThreadsAwarenessHandler.kt      |  2 +-
 .../features/home/room/detail/RoomDetailViewModel.kt  |  2 +-
 8 files changed, 33 insertions(+), 12 deletions(-)

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 5f9a15de02..7372a83873 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
@@ -367,9 +367,9 @@ fun Event.isReply(): Boolean {
     return getRelationContent()?.inReplyTo?.eventId != null
 }
 
-fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
 
-fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
 
 fun Event.isEdition(): Boolean {
     return getRelationContentForType(RelationType.REPLACE)?.eventId != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
index 6546258766..18bb946462 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
@@ -29,8 +29,8 @@ object RelationType {
     const val REFERENCE = "m.reference"
 
     /** Lets you define an event which is a reply to an existing event.*/
-//    const val THREAD = "m.thread"
-    const val THREAD = "io.element.thread"
+    const val THREAD = "m.thread"
+    const val IO_THREAD = "io.element.thread"
 
     /** Lets you define an event which adds a response to an existing event.*/
     const val RESPONSE = "org.matrix.response"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index 7047d38260..f498322967 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -48,6 +48,16 @@ data class RoomEventFilter(
          * a wildcard to match any sequence of characters.
          */
         @Json(name = "types") val types: List? = null,
+        /**
+         * A list of relation types which must be exist pointing to the event being filtered.
+         * If this list is absent then no filtering is done on relation types.
+         */
+        @Json(name = "relation_types") val relationTypes: List? = null,
+        /**
+         *  A list of senders of relations which must exist pointing to the event being filtered.
+         *  If this list is absent then no filtering is done on relation types.
+         */
+        @Json(name = "relation_senders") val relationSenders: List? = null,
         /**
          * A list of room IDs to include. If this list is absent then all rooms are included.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index efc5166a0c..0017cdd917 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -376,4 +376,15 @@ internal interface RoomAPI {
     @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "im.nheko.summary/rooms/{roomIdOrAlias}/summary")
     suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String,
                                @Query("via") viaServers: List?): RoomStrippedState
+
+    // TODO add doc
+    /**
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/messages")
+    suspend fun getRoomThreadMessages(@Path("roomId") roomId: String,
+                                      @Query("from") from: String,
+                                      @Query("dir") dir: String,
+                                      @Query("limit") limit: Int,
+                                      @Query("filter") filter: String?
+    ): PaginationResponse
 }
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 1046bcee49..aad1d422a6 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
@@ -290,7 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         size = attachment.size
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) }
+                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
         )
         return createMessageEvent(roomId, content)
     }
@@ -327,7 +327,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         thumbnailInfo = thumbnailInfo
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) }
+                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
         )
         return createMessageEvent(roomId, content)
     }
@@ -351,7 +351,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         waveform = waveformSanitizer.sanitize(attachment.waveform)
                 ),
                 voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
-                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) }
+                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
         )
         return createMessageEvent(roomId, content)
     }
@@ -365,7 +365,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         size = attachment.size
                 ),
                 url = attachment.queryUri.toString(),
-                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.THREAD, it) }
+                relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
         )
         return createMessageEvent(roomId, content)
     }
@@ -454,7 +454,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null): RelationDefaultContent =
             rootThreadEventId?.let {
                 RelationDefaultContent(
-                        type = RelationType.THREAD,
+                        type = RelationType.IO_THREAD,
                         eventId = it,
                         inReplyTo = ReplyToContent(eventId))
             } ?: RelationDefaultContent(null, null, ReplyToContent(eventId))
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 d3e0189f4b..0bf0561599 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
@@ -48,7 +48,7 @@ fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String =
             msgType = msgType,
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
             body = text,
-            relatesTo = RelationDefaultContent(RelationType.THREAD, rootThreadEventId),
+            relatesTo = RelationDefaultContent(RelationType.IO_THREAD, rootThreadEventId),
             formattedBody = formattedText
     )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index a4ebfabc5c..24854b601f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -272,7 +272,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event
      */
     private fun isThreadEvent(event: Event): Boolean =
-            event.content.toModel()?.relatesTo?.type == RelationType.THREAD
+            event.content.toModel()?.relatesTo?.type == RelationType.IO_THREAD
 
     /**
      * Returns the root thread eventId or null otherwise
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 3a19106312..62bcccb67a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -436,7 +436,7 @@ class RoomDetailViewModel @AssistedInject constructor(
 
     private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
         val content = initialState.rootThreadEventId?.let {
-            action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.THREAD, it))
+            action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.IO_THREAD, it))
         } ?: action.stickerContent
         room.sendEvent(EventType.STICKER, content.toContent())
     }

From d7546db26f917a630c09d4d992e1abd801ce5a25 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 21 Dec 2021 20:09:25 +0200
Subject: [PATCH 039/130] Fix code quality issues

---
 .../home/room/threads/ThreadsActivity.kt       |  6 +++++-
 .../res/layout/view_room_detail_toolbar.xml    | 18 +++++++++---------
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index 84d270a2c2..fe3c32ae65 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -102,7 +102,11 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
                 avatarUrl = timelineEvent.senderInfo.avatarUrl,
                 rootThreadEventId = timelineEvent.eventId)
         val commonOption: (FragmentTransaction) -> Unit = {
-            it.setCustomAnimations(R.anim.animation_slide_in_right, R.anim.animation_slide_out_left, R.anim.animation_slide_in_left, R.anim.animation_slide_out_right)
+            it.setCustomAnimations(
+                    R.anim.animation_slide_in_right,
+                    R.anim.animation_slide_out_left,
+                    R.anim.animation_slide_in_left,
+                    R.anim.animation_slide_out_right)
         }
         addFragmentToBackstack(
                 container = views.threadsActivityFragmentContainer,
diff --git a/vector/src/main/res/layout/view_room_detail_toolbar.xml b/vector/src/main/res/layout/view_room_detail_toolbar.xml
index fdc3f6819e..ab78f45243 100644
--- a/vector/src/main/res/layout/view_room_detail_toolbar.xml
+++ b/vector/src/main/res/layout/view_room_detail_toolbar.xml
@@ -25,9 +25,9 @@
         android:layout_height="17dp"
         android:layout_marginStart="5dp"
         android:layout_marginTop="2dp"
-        app:layout_constraintBottom_toBottomOf="@+id/roomToolbarTitleView"
-        app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
-        app:layout_constraintTop_toTopOf="@+id/roomToolbarTitleView" />
+        app:layout_constraintBottom_toBottomOf="@id/roomToolbarTitleView"
+        app:layout_constraintStart_toEndOf="@id/roomToolbarAvatarImageView"
+        app:layout_constraintTop_toTopOf="@id/roomToolbarTitleView" />
 
     
 

From f06397023a8b7e5d984270a97567820bc1608ab7 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 23 Dec 2021 17:19:36 +0200
Subject: [PATCH 040/130] Add support when there no threads messages to init
 timeline. Init as the normal one and hide them on the app side. That is also
 helpful to work to load all the threads when there is no server support

---
 .../room/model/relation/RelationService.kt    | 12 +++
 .../database/helper/ChunkEntityHelper.kt      |  4 +-
 .../sdk/internal/session/room/RoomAPI.kt      | 13 +--
 .../sdk/internal/session/room/RoomModule.kt   |  5 +
 .../room/relation/DefaultRelationService.kt   | 97 ++++++++++++++++++-
 .../threads/FetchThreadTimelineTask.kt        | 55 +++++++++++
 .../session/room/timeline/DefaultTimeline.kt  | 29 ++++--
 .../home/room/detail/RoomDetailViewModel.kt   | 46 ++++++---
 .../timeline/TimelineEventController.kt       | 26 ++++-
 .../factory/MergedHeaderItemFactory.kt        |  2 +-
 .../timeline/factory/TimelineItemFactory.kt   | 32 +++++-
 .../helper/TimelineEventVisibilityHelper.kt   | 44 +++++++--
 12 files changed, 313 insertions(+), 52 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt

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 a5ecfaf6e4..4f28f7dce1 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
@@ -145,4 +145,16 @@ interface RelationService {
                       autoMarkdown: Boolean = false,
                       formattedText: String? = null,
                       eventReplied: TimelineEvent? = null): Cancelable?
+
+
+
+
+    /**
+     * Get all the thread replies for the specified rootThreadEventId
+     * The return list will contain the original root thread event and all the thread replies to that event
+     * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
+     * from the backend
+     * @param rootThreadEventId the root thread eventId
+     */
+    suspend fun fetchThreadTimeline(rootThreadEventId: String): List
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index b0d15ce8da..0b8c42c8cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -82,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
 internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           eventEntity: EventEntity,
                                           direction: PaginationDirection,
-                                          roomMemberContentsByUser: Map) {
+                                          roomMemberContentsByUser: Map? = null) {
     val eventId = eventEntity.eventId
     if (timelineEvents.find(eventId) != null) {
         return
@@ -102,7 +102,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                 ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
-        val roomMemberContent = roomMemberContentsByUser[senderId]
+        val roomMemberContent = roomMemberContentsByUser?.get(senderId)
         this.senderAvatar = roomMemberContent?.avatarUrl
         this.senderName = roomMemberContent?.displayName
         isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 0017cdd917..2dd1871ac0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -226,7 +226,8 @@ internal interface RoomAPI {
     suspend fun getRelations(@Path("roomId") roomId: String,
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
-                             @Path("eventType") eventType: String
+                             @Path("eventType") eventType: String,
+                             @Query("limit") limit: Int?= null
     ): RelationsResponse
 
     /**
@@ -377,14 +378,4 @@ internal interface RoomAPI {
     suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String,
                                @Query("via") viaServers: List?): RoomStrippedState
 
-    // TODO add doc
-    /**
-     */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/messages")
-    suspend fun getRoomThreadMessages(@Path("roomId") roomId: String,
-                                      @Query("from") from: String,
-                                      @Query("dir") dir: String,
-                                      @Query("limit") limit: Int,
-                                      @Query("filter") filter: String?
-    ): PaginationResponse
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index dbd0ae6f06..7939c74dce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -74,6 +74,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
 import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
 import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
 import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
 import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
@@ -256,4 +258,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
+
+    @Binds
+    abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
 }
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 d459e79a4a..02af20de23 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
@@ -21,26 +21,48 @@ import com.zhuinden.monarchy.Monarchy
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import io.realm.Realm
 import org.matrix.android.sdk.api.MatrixCallback
+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.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.relation.RelationService
+import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.NoOpCancellable
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 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.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 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.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.findIncludingEvent
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+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.MoshiProvider
 import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 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.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
+import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.fetchCopyMap
 import timber.log.Timber
 
@@ -50,9 +72,12 @@ internal class DefaultRelationService @AssistedInject constructor(
         private val eventSenderProcessor: EventSenderProcessor,
         private val eventFactory: LocalEchoEventFactory,
         private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
+        private val cryptoService: DefaultCryptoService,
         private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
         private val fetchEditHistoryTask: FetchEditHistoryTask,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventMapper: TimelineEventMapper,
+        @UserId private val userId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val taskExecutor: TaskExecutor) :
         RelationService {
@@ -192,7 +217,77 @@ internal class DefaultRelationService @AssistedInject constructor(
                         saveLocalEcho(it)
                     }
         }
-        return  eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+    }
+
+    private fun decryptIfNeeded(event: Event, roomId: String) {
+        try {
+            // Event from sync does not have roomId, so add it to the event first
+            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
+            event.mxDecryptionResult = OlmDecryptionResult(
+                    payload = result.clearEvent,
+                    senderKey = result.senderCurve25519Key,
+                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+            )
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String): List {
+        val results = fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
+        var counter = 0
+//
+//        monarchy
+//                .awaitTransaction { realm ->
+//                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
+//
+//                    val optimizedThreadSummaryMap = hashMapOf()
+//                    for (event in results.reversed()) {
+//                        if (event.eventId == null || event.senderId == null || event.type == null) {
+//                            continue
+//                        }
+//
+//                        // skip if event already exists
+//                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
+//                            counter++
+//                            continue
+//                        }
+//
+//                        if (event.isEncrypted()) {
+//                            decryptIfNeeded(event, roomId)
+//                        }
+//
+//                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+//                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+//                        if (event.stateKey != null) {
+//                            CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+//                                eventId = event.eventId
+//                                root = eventEntity
+//                            }
+//                        }
+//                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS)
+//                        eventEntity.rootThreadEventId?.let {
+//                            // This is a thread event
+//                            optimizedThreadSummaryMap[it] = eventEntity
+//                        } ?: run {
+//                            // This is a normal event or a root thread one
+//                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+//                        }
+//                    }
+//
+//                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+//                            roomId = roomId,
+//                            realm = realm,
+//                            currentUserId = userId)
+//                }
+        Timber.i("----> size: ${results.size} | skipped: $counter | threads: ${results.map{ it.eventId}}")
+
+        return results
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
new file mode 100644
index 0000000000..d62ce4158f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.internal.session.room.relation.threads
+
+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.RelationType
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface FetchThreadTimelineTask : Task> {
+    data class Params(
+            val roomId: String,
+            val rootThreadEventId: String
+    )
+}
+
+internal class DefaultFetchThreadTimelineTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
+) : FetchThreadTimelineTask {
+
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): List {
+        val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
+        val response = executeRequest(globalErrorReceiver) {
+            roomAPI.getRelations(
+                    roomId = params.roomId,
+                    eventId = params.rootThreadEventId,
+                    relationType = RelationType.IO_THREAD,
+                    eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
+                    limit = 2000
+            )
+        }
+
+        return response.chunks + listOfNotNull(response.originalEvent)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 69e56a85d0..a100c4635a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.util.Debouncer
 import org.matrix.android.sdk.internal.util.createBackgroundHandler
 import org.matrix.android.sdk.internal.util.createUIHandler
 import timber.log.Timber
+import java.lang.Thread.sleep
 import java.util.Collections
 import java.util.UUID
 import java.util.concurrent.CopyOnWriteArrayList
@@ -107,6 +108,7 @@ internal class DefaultTimeline(
     private val backwardsState = AtomicReference(TimelineState())
     private val forwardsState = AtomicReference(TimelineState())
     private var isFromThreadTimeline = false
+    private var rootThreadEventId: String? = null
     override val timelineID = UUID.randomUUID().toString()
 
     override val isLive
@@ -151,9 +153,11 @@ internal class DefaultTimeline(
     override fun start(rootThreadEventId: String?) {
         if (isStarted.compareAndSet(false, true)) {
             isFromThreadTimeline = rootThreadEventId != null
+            this@DefaultTimeline.rootThreadEventId = rootThreadEventId
             Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
             timelineInput.listeners.add(this)
             BACKGROUND_HANDLER.post {
+
                 eventDecryptor.start()
                 val realm = Realm.getInstance(realmConfiguration)
                 backgroundRealm.set(realm)
@@ -170,9 +174,10 @@ internal class DefaultTimeline(
                 }
 
                 timelineEvents = rootThreadEventId?.let {
-                    TimelineEventEntity
+                    val threadTimelineEvents = TimelineEventEntity
                             .whereRoomId(realm, roomId = roomId)
                             .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
+//                            .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(it))
                             .beginGroup()
                             .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
                             .or()
@@ -180,7 +185,15 @@ internal class DefaultTimeline(
                             .endGroup()
                             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                             .findAll()
+                    if (threadTimelineEvents.isNullOrEmpty()) {
+                        // When there no threads in the last forward chunk get all events and hide them
+                        buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
+                    } else {
+                        threadTimelineEvents
+                    }
                 } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
+                if (isFromThreadTimeline)
+                    Timber.i("----> timelineEvents.size: ${timelineEvents.size}")
 
                 timelineEvents.addChangeListener(eventsChangeListener)
                 handleInitialLoad()
@@ -330,17 +343,19 @@ internal class DefaultTimeline(
         val lastCacheEvent = results.lastOrNull()
         val firstCacheEvent = results.firstOrNull()
         val chunkEntity = getLiveChunk()
+        if (isFromThreadTimeline)
+            Timber.i("----> results.size: ${results.size} | contains root thread ${results.map { it.eventId }.contains(rootThreadEventId)}")
 
-        updateState(Timeline.Direction.FORWARDS) {
-            it.copy(
+        updateState(Timeline.Direction.FORWARDS) { state ->
+            state.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),   // what is in DB
                     hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more
             )
         }
-        updateState(Timeline.Direction.BACKWARDS) {
-            it.copy(
+        updateState(Timeline.Direction.BACKWARDS) { state ->
+            state.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
-                    hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
+                    hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) true else (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
             )
         }
     }
@@ -640,7 +655,7 @@ internal class DefaultTimeline(
                 }.map {
                     EventMapper.map(it)
                 }
-            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
+        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
     }
 
     private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 62bcccb67a..552a7e63f6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -180,6 +180,15 @@ class RoomDetailViewModel @AssistedInject constructor(
         if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
             prepareForEncryption()
         }
+
+        // Threads
+        initThreads()
+    }
+
+    /**
+     * Threads specific initialization
+     */
+    private fun initThreads() {
         markThreadTimelineAsReadLocal()
         observeLocalThreadNotifications()
     }
@@ -269,6 +278,18 @@ class RoomDetailViewModel @AssistedInject constructor(
                 }
     }
 
+    /**
+     * Mark the thread as read, while the user navigated within the thread
+     * This is a local implementation has nothing to do with APIs
+     */
+    private fun markThreadTimelineAsReadLocal() {
+        initialState.rootThreadEventId?.let {
+            session.coroutineScope.launch {
+                room.markThreadAsRead(it)
+            }
+        }
+    }
+
     /**
      * Observe local unread threads
      */
@@ -287,6 +308,17 @@ class RoomDetailViewModel @AssistedInject constructor(
                 }
     }
 
+//    /**
+//     * Fetch all the thread replies for the current thread
+//     */
+//    private fun fetchThreadTimeline() {
+//        initialState.rootThreadEventId?.let {
+//            viewModelScope.launch(Dispatchers.IO) {
+//                room.fetchThreadTimeline(it)
+//            }
+//        }
+//    }
+
     fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
 
     fun getRoomSummary() = room.roomSummary()
@@ -1076,18 +1108,6 @@ class RoomDetailViewModel @AssistedInject constructor(
         }
     }
 
-    /**
-     * Mark the thread as read, while the user navigated within the thread
-     * This is a local implementation has nothing to do with APIs
-     */
-    private fun markThreadTimelineAsReadLocal() {
-        initialState.rootThreadEventId?.let {
-            session.coroutineScope.launch {
-                room.markThreadAsRead(it)
-            }
-        }
-    }
-
     override fun onTimelineUpdated(snapshot: List) {
         viewModelScope.launch {
             // tryEmit doesn't work with SharedFlow without cache
@@ -1125,6 +1145,8 @@ class RoomDetailViewModel @AssistedInject constructor(
         chatEffectManager.delegate = null
         chatEffectManager.dispose()
         callManager.removeProtocolsCheckerListener(this)
+        // we should also mark it as read here, for the scenario that the user
+        // is already in the thread timeline
         markThreadTimelineAsReadLocal()
         super.onCleared()
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 20a3f34338..b29cf141d4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -200,7 +200,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                 // it's sent by the same user so we are sure we have up to date information.
                 val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
                 val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
-                    timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline())
+                    timelineEventVisibilityHelper.shouldShowEvent(
+                            timelineEvent = it,
+                            highlightedEventId = partialState.highlightedEventId,
+                            isFromThreadTimeline = partialState.isFromThreadTimeline(),
+                            rootThreadEventId = partialState.rootThreadEventId
+                    )
                 }
                 if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
                     modelCache[prevDisplayableEventIndex] = null
@@ -377,7 +382,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             val nextEvent = currentSnapshot.nextOrNull(position)
             val prevEvent = currentSnapshot.prevOrNull(position)
             val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
-                timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline())
+                timelineEventVisibilityHelper.shouldShowEvent(
+                        timelineEvent = it,
+                        highlightedEventId = partialState.highlightedEventId,
+                        isFromThreadTimeline = partialState.isFromThreadTimeline(),
+                        rootThreadEventId = partialState.rootThreadEventId)
             }
             // Should be build if not cached or if model should be refreshed
             if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
@@ -459,7 +468,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                 return null
             }
             // If the event is not shown, we go to the next one
-            if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) {
+            if (!timelineEventVisibilityHelper.shouldShowEvent(
+                            timelineEvent = event,
+                            highlightedEventId = partialState.highlightedEventId,
+                            isFromThreadTimeline = partialState.isFromThreadTimeline(),
+                            rootThreadEventId = partialState.rootThreadEventId
+                    )) {
                 continue
             }
             // If the event is sent by us, we update the holder with the eventId and stop the search
@@ -481,7 +495,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             val currentReadReceipts = ArrayList(event.readReceipts).filter {
                 it.user.userId != session.myUserId
             }
-            if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) {
+            if (timelineEventVisibilityHelper.shouldShowEvent(
+                            timelineEvent = event,
+                            highlightedEventId = partialState.highlightedEventId,
+                            isFromThreadTimeline = partialState.isFromThreadTimeline(),
+                            rootThreadEventId = partialState.rootThreadEventId)) {
                 lastShownEventId = event.eventId
             }
             if (lastShownEventId == null) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 1c25f923cf..874d8f0b1e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
                                                    eventIdToHighlight: String?,
                                                    requestModelBuild: () -> Unit,
                                                    callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
-        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.isFromThreadTimeline())
+        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight,partialState.rootThreadEventId, partialState.isFromThreadTimeline())
         return if (mergedEvents.isEmpty()) {
             null
         } else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 2f7c7fdc0f..1e915d2b29 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -42,8 +42,17 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
     fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
         val event = params.event
         val computedModel = try {
-            if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.isFromThreadTimeline())) {
-                return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.isFromThreadTimeline())
+            if (!timelineEventVisibilityHelper.shouldShowEvent(
+                            timelineEvent = event,
+                            highlightedEventId = params.highlightedEventId,
+                            isFromThreadTimeline = params.isFromThreadTimeline(),
+                            rootThreadEventId = params.rootThreadEventId)) {
+                return buildEmptyItem(
+                        event,
+                        params.prevEvent,
+                        params.highlightedEventId,
+                        params.rootThreadEventId,
+                        params.isFromThreadTimeline())
             }
             when (event.root.getClearType()) {
                 // Message itemsX
@@ -112,11 +121,24 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
             Timber.e(throwable, "failed to create message item")
             defaultItemFactory.create(params, throwable)
         }
-        return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.isFromThreadTimeline())
+        return computedModel ?: buildEmptyItem(
+                event,
+                params.prevEvent,
+                params.highlightedEventId,
+                params.rootThreadEventId,
+                params.isFromThreadTimeline())
     }
 
-    private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, isFromThreadTimeline: Boolean): TimelineEmptyItem {
-        val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, isFromThreadTimeline)
+    private fun buildEmptyItem(timelineEvent: TimelineEvent,
+                               prevEvent: TimelineEvent?,
+                               highlightedEventId: String?,
+                               rootThreadEventId: String?,
+                               isFromThreadTimeline: Boolean): TimelineEmptyItem {
+        val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(
+                timelineEvent = prevEvent,
+                highlightedEventId = highlightedEventId,
+                isFromThreadTimeline = isFromThreadTimeline,
+                rootThreadEventId = rootThreadEventId)
         return TimelineEmptyItem_()
                 .id(timelineEvent.localId)
                 .eventId(timelineEvent.eventId)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 7efefc5209..e91f28cea6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -40,7 +40,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
      *
      * @return a list of timeline events which have sequentially the same type following the next direction.
      */
-    private fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List {
+    private fun nextSameTypeEvents(
+            timelineEvents: List,
+            index: Int,
+            minSize: Int,
+            eventIdToHighlight: String?,
+            rootThreadEventId: String?,
+            isFromThreadTimeline: Boolean): List {
         if (index >= timelineEvents.size - 1) {
             return emptyList()
         }
@@ -62,11 +68,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
         } else {
             nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
         }
-        val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, isFromThreadTimeline) }
+        val filteredSameTypeEvents = sameTypeEvents.filter {
+            shouldShowEvent(
+                    timelineEvent = it,
+                    highlightedEventId = eventIdToHighlight,
+                    isFromThreadTimeline = isFromThreadTimeline,
+                    rootThreadEventId = rootThreadEventId
+            )
+        }
         if (filteredSameTypeEvents.size < minSize) {
             return emptyList()
         }
-        return  filteredSameTypeEvents
+        return filteredSameTypeEvents
     }
 
     /**
@@ -77,12 +90,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
      *
      * @return a list of timeline events which have sequentially the same type following the prev direction.
      */
-    fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List {
+    fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, isFromThreadTimeline: Boolean): List {
         val prevSub = timelineEvents.subList(0, index + 1)
         return prevSub
                 .reversed()
                 .let {
-                    nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, isFromThreadTimeline)
+                    nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline)
                 }
     }
 
@@ -92,7 +105,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
      * @param rootThreadEventId if this param is null it means we are in the original timeline
      * @return true if the event should be shown in the timeline.
      */
-    fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, isFromThreadTimeline: Boolean): Boolean {
+    fun shouldShowEvent(
+            timelineEvent: TimelineEvent,
+            highlightedEventId: String?,
+            isFromThreadTimeline: Boolean,
+            rootThreadEventId: String?
+    ): Boolean {
         // If show hidden events is true we should always display something
         if (userPreferencesProvider.shouldShowHiddenEvents()) {
             return true
@@ -106,14 +124,14 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
         }
 
         // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences.
-        return !timelineEvent.shouldBeHidden(isFromThreadTimeline)
+        return !timelineEvent.shouldBeHidden(rootThreadEventId, isFromThreadTimeline)
     }
 
     private fun TimelineEvent.isDisplayable(): Boolean {
         return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
     }
 
-    private fun TimelineEvent.shouldBeHidden(isFromThreadTimeline: Boolean): Boolean {
+    private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?, isFromThreadTimeline: Boolean): Boolean {
         if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
             return true
         }
@@ -128,10 +146,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
             return true
         }
 
-        if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null) {
+        if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread()) {
             return true
         }
 
+        if (BuildConfig.THREADING_ENABLED && isFromThreadTimeline) {
+
+            ////
+            return if (root.getRootThreadEventId() == rootThreadEventId) {
+                false
+            } else root.eventId != rootThreadEventId
+        }
+
         return false
     }
 

From 581f71e89d53b9ddedd31a27b9bee02de9c0fbc8 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 23 Dec 2021 17:22:27 +0200
Subject: [PATCH 041/130] Remove unused code

---
 .../room/relation/DefaultRelationService.kt   | 68 +------------------
 .../session/room/timeline/DefaultTimeline.kt  |  5 --
 .../home/room/detail/RoomDetailViewModel.kt   | 11 ---
 3 files changed, 1 insertion(+), 83 deletions(-)

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 02af20de23..2a6950d742 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
@@ -220,74 +220,8 @@ internal class DefaultRelationService @AssistedInject constructor(
         return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
-        try {
-            // Event from sync does not have roomId, so add it to the event first
-            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
-            event.mxDecryptionResult = OlmDecryptionResult(
-                    payload = result.clearEvent,
-                    senderKey = result.senderCurve25519Key,
-                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
-                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
-            )
-        } catch (e: MXCryptoError) {
-            if (e is MXCryptoError.Base) {
-                event.mCryptoError = e.errorType
-                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
-            }
-        }
-    }
-
     override suspend fun fetchThreadTimeline(rootThreadEventId: String): List {
-        val results = fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
-        var counter = 0
-//
-//        monarchy
-//                .awaitTransaction { realm ->
-//                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-//
-//                    val optimizedThreadSummaryMap = hashMapOf()
-//                    for (event in results.reversed()) {
-//                        if (event.eventId == null || event.senderId == null || event.type == null) {
-//                            continue
-//                        }
-//
-//                        // skip if event already exists
-//                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
-//                            counter++
-//                            continue
-//                        }
-//
-//                        if (event.isEncrypted()) {
-//                            decryptIfNeeded(event, roomId)
-//                        }
-//
-//                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-//                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
-//                        if (event.stateKey != null) {
-//                            CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
-//                                eventId = event.eventId
-//                                root = eventEntity
-//                            }
-//                        }
-//                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS)
-//                        eventEntity.rootThreadEventId?.let {
-//                            // This is a thread event
-//                            optimizedThreadSummaryMap[it] = eventEntity
-//                        } ?: run {
-//                            // This is a normal event or a root thread one
-//                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
-//                        }
-//                    }
-//
-//                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
-//                            roomId = roomId,
-//                            realm = realm,
-//                            currentUserId = userId)
-//                }
-        Timber.i("----> size: ${results.size} | skipped: $counter | threads: ${results.map{ it.eventId}}")
-
-        return results
+        return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index a100c4635a..72a922dc88 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -177,7 +177,6 @@ internal class DefaultTimeline(
                     val threadTimelineEvents = TimelineEventEntity
                             .whereRoomId(realm, roomId = roomId)
                             .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
-//                            .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(it))
                             .beginGroup()
                             .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
                             .or()
@@ -192,8 +191,6 @@ internal class DefaultTimeline(
                         threadTimelineEvents
                     }
                 } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
-                if (isFromThreadTimeline)
-                    Timber.i("----> timelineEvents.size: ${timelineEvents.size}")
 
                 timelineEvents.addChangeListener(eventsChangeListener)
                 handleInitialLoad()
@@ -343,8 +340,6 @@ internal class DefaultTimeline(
         val lastCacheEvent = results.lastOrNull()
         val firstCacheEvent = results.firstOrNull()
         val chunkEntity = getLiveChunk()
-        if (isFromThreadTimeline)
-            Timber.i("----> results.size: ${results.size} | contains root thread ${results.map { it.eventId }.contains(rootThreadEventId)}")
 
         updateState(Timeline.Direction.FORWARDS) { state ->
             state.copy(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 552a7e63f6..45389fefc8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -308,17 +308,6 @@ class RoomDetailViewModel @AssistedInject constructor(
                 }
     }
 
-//    /**
-//     * Fetch all the thread replies for the current thread
-//     */
-//    private fun fetchThreadTimeline() {
-//        initialState.rootThreadEventId?.let {
-//            viewModelScope.launch(Dispatchers.IO) {
-//                room.fetchThreadTimeline(it)
-//            }
-//        }
-//    }
-
     fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
 
     fun getRoomSummary() = room.roomSummary()

From d3e9e197798a26a1beae4b1cfac27f5d162199ea Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Sat, 25 Dec 2021 13:11:42 +0200
Subject: [PATCH 042/130] Fix code quality issues

---
 .../helper/TimelineEventVisibilityHelper.kt   |  8 ++-
 .../view_thread_notification_badge_old.xml    | 53 -------------------
 2 files changed, 7 insertions(+), 54 deletions(-)
 delete mode 100644 vector/src/main/res/layout/view_thread_notification_badge_old.xml

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index e91f28cea6..1ffd66c572 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -90,7 +90,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
      *
      * @return a list of timeline events which have sequentially the same type following the prev direction.
      */
-    fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, isFromThreadTimeline: Boolean): List {
+    fun prevSameTypeEvents(
+            timelineEvents: List,
+            index: Int,
+            minSize: Int,
+            eventIdToHighlight: String?,
+            rootThreadEventId: String?,
+            isFromThreadTimeline: Boolean): List {
         val prevSub = timelineEvents.subList(0, index + 1)
         return prevSub
                 .reversed()
diff --git a/vector/src/main/res/layout/view_thread_notification_badge_old.xml b/vector/src/main/res/layout/view_thread_notification_badge_old.xml
deleted file mode 100644
index 70efceda51..0000000000
--- a/vector/src/main/res/layout/view_thread_notification_badge_old.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-    
-
-    
-
-    
-
-
-

From 9ef4e1e83f4dd296797d46a15ab30f06e89f8497 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Sat, 25 Dec 2021 13:42:53 +0200
Subject: [PATCH 043/130] Fix code quality issues

---
 .../room/model/relation/RelationService.kt    |  3 ---
 .../sdk/internal/session/room/RoomAPI.kt      |  3 +--
 .../room/relation/DefaultRelationService.kt   | 19 -------------------
 .../session/room/timeline/DefaultTimeline.kt  |  2 --
 .../factory/MergedHeaderItemFactory.kt        |  2 +-
 .../helper/TimelineEventVisibilityHelper.kt   |  3 +--
 6 files changed, 3 insertions(+), 29 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 4f28f7dce1..183cd481d2 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
@@ -146,9 +146,6 @@ interface RelationService {
                       formattedText: String? = null,
                       eventReplied: TimelineEvent? = null): Cancelable?
 
-
-
-
     /**
      * Get all the thread replies for the specified rootThreadEventId
      * The return list will contain the original root thread event and all the thread replies to that event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 2dd1871ac0..399bfbd0e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -227,7 +227,7 @@ internal interface RoomAPI {
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
                              @Path("eventType") eventType: String,
-                             @Query("limit") limit: Int?= null
+                             @Query("limit") limit: Int? = null
     ): RelationsResponse
 
     /**
@@ -377,5 +377,4 @@ internal interface RoomAPI {
     @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "im.nheko.summary/rooms/{roomIdOrAlias}/summary")
     suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String,
                                @Query("via") viaServers: List?): RoomStrippedState
-
 }
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 2a6950d742..47794e424f 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
@@ -21,13 +21,10 @@ import com.zhuinden.monarchy.Monarchy
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import io.realm.Realm
 import org.matrix.android.sdk.api.MatrixCallback
-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.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.relation.RelationService
-import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.NoOpCancellable
@@ -35,34 +32,18 @@ import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 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.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 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.EventAnnotationsSummaryEntity
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
-import org.matrix.android.sdk.internal.database.query.findIncludingEvent
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
-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.MoshiProvider
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 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.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
-import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.fetchCopyMap
 import timber.log.Timber
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 72a922dc88..00e7510b00 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -53,7 +53,6 @@ import org.matrix.android.sdk.internal.util.Debouncer
 import org.matrix.android.sdk.internal.util.createBackgroundHandler
 import org.matrix.android.sdk.internal.util.createUIHandler
 import timber.log.Timber
-import java.lang.Thread.sleep
 import java.util.Collections
 import java.util.UUID
 import java.util.concurrent.CopyOnWriteArrayList
@@ -157,7 +156,6 @@ internal class DefaultTimeline(
             Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
             timelineInput.listeners.add(this)
             BACKGROUND_HANDLER.post {
-
                 eventDecryptor.start()
                 val realm = Realm.getInstance(realmConfiguration)
                 backgroundRealm.set(realm)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 874d8f0b1e..6ef8ba5a0f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
                                                    eventIdToHighlight: String?,
                                                    requestModelBuild: () -> Unit,
                                                    callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
-        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight,partialState.rootThreadEventId, partialState.isFromThreadTimeline())
+        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.rootThreadEventId, partialState.isFromThreadTimeline())
         return if (mergedEvents.isEmpty()) {
             null
         } else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 1ffd66c572..6d23d22ff0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -157,8 +157,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
         }
 
         if (BuildConfig.THREADING_ENABLED && isFromThreadTimeline) {
-
-            ////
+            // //
             return if (root.getRootThreadEventId() == rootThreadEventId) {
                 false
             } else root.eventId != rootThreadEventId

From 0e30f4e817db7e5c329cceab3970e7a04105a4af Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Sat, 25 Dec 2021 23:35:40 +0200
Subject: [PATCH 044/130] Fix code quality issues

---
 .../session/room/timeline/DefaultTimeline.kt      |  8 ++++++--
 .../features/home/room/detail/TimelineFragment.kt | 15 ++++++++++-----
 .../timeline/factory/MergedHeaderItemFactory.kt   |  8 +++++++-
 3 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 00e7510b00..3807ffe507 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -342,13 +342,17 @@ internal class DefaultTimeline(
         updateState(Timeline.Direction.FORWARDS) { state ->
             state.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),   // what is in DB
-                    hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more
+                    hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you need fetch more
             )
         }
         updateState(Timeline.Direction.BACKWARDS) { state ->
             state.copy(
                     hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
-                    hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) true else (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
+                    hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) {
+                        true
+                    } else {
+                        (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
+                    }
             )
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 7778294aa3..652451c1ff 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -734,27 +734,32 @@ class TimelineFragment @Inject constructor(
             }
 
             override fun onSendVoiceMessage() {
-                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId()))
+                messageComposerViewModel.handle(
+                        MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId()))
                 updateRecordingUiState(RecordingUiState.Idle)
             }
 
             override fun onDeleteVoiceMessage() {
-                messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
+                messageComposerViewModel.handle(
+                        MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()))
                 updateRecordingUiState(RecordingUiState.Idle)
             }
 
             override fun onRecordingLimitReached() {
-                messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
+                messageComposerViewModel.handle(
+                        MessageComposerAction.PauseRecordingVoiceMessage)
                 updateRecordingUiState(RecordingUiState.Draft)
             }
 
             override fun onRecordingWaveformClicked() {
-                messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
+                messageComposerViewModel.handle(
+                        MessageComposerAction.PauseRecordingVoiceMessage)
                 updateRecordingUiState(RecordingUiState.Draft)
             }
 
             private fun updateRecordingUiState(state: RecordingUiState) {
-                messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
+                messageComposerViewModel.handle(
+                        MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 6ef8ba5a0f..99a026a0cf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -83,7 +83,13 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
                                                    eventIdToHighlight: String?,
                                                    requestModelBuild: () -> Unit,
                                                    callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
-        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.rootThreadEventId, partialState.isFromThreadTimeline())
+        val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
+                items,
+                currentPosition,
+                2,
+                eventIdToHighlight,
+                partialState.rootThreadEventId,
+                partialState.isFromThreadTimeline())
         return if (mergedEvents.isEmpty()) {
             null
         } else {

From 0d9bc188d7700657c74786125ad769dae8ba7f24 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Sun, 26 Dec 2021 00:48:11 +0200
Subject: [PATCH 045/130] Fix code quality issues

---
 tools/check/forbidden_strings_in_code.txt   | 2 +-
 tools/check/forbidden_strings_in_layout.txt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
 mode change 100644 => 100755 tools/check/forbidden_strings_in_code.txt
 mode change 100644 => 100755 tools/check/forbidden_strings_in_layout.txt

diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
old mode 100644
new mode 100755
index 6ca86be095..7b869e8cd2
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
 # android\.text\.TextUtils
 
 ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
-enum class===114
+enum class===117
 
 ### Do not import temporary legacy classes
 import org.matrix.android.sdk.internal.legacy.riot===3
diff --git a/tools/check/forbidden_strings_in_layout.txt b/tools/check/forbidden_strings_in_layout.txt
old mode 100644
new mode 100755
index 545983f844..e46aa3a0bb
--- a/tools/check/forbidden_strings_in_layout.txt
+++ b/tools/check/forbidden_strings_in_layout.txt
@@ -24,7 +24,7 @@
 # Extension:xml
 
 ### Use style="@style/Widget.Vector.TextView.*" instead of textSize attribute
-android:textSize===9
+android:textSize===11
 
 ### Use `@id` and not `@+id` when referencing ids in layouts
 layout_(.*)="@\+id

From f9e03aa99ef268c2a958a42984bc92fea33e06d6 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 11:33:38 +0200
Subject: [PATCH 046/130] Remove unused code

---
 .../matrix/android/sdk/internal/database/model/EventEntity.kt   | 2 --
 1 file changed, 2 deletions(-)

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 f4e12bf3ed..2c103652d0 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
@@ -48,9 +48,7 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 @Index var isRootThread: Boolean = false,
                                 @Index var rootThreadEventId: String? = null,
                                 var numberOfThreads: Int = 0,
-//                                var threadNotificationState: Boolean = false,
                                 var threadSummaryLatestMessage: TimelineEventEntity? = null
-
 ) : RealmObject() {
 
     private var sendStateStr: String = SendState.UNKNOWN.name

From c2183800d3261689b95e83ba848c5c6f165e55f5 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:14:14 +0200
Subject: [PATCH 047/130] Github actions improvement test

---
 .github/workflows/integration.yml | 15 ++++++++++-----
 matrix-sdk-android/build.gradle   |  2 +-
 vector/build.gradle               |  2 +-
 3 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index c18ca69fde..5cc8882e20 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -66,11 +66,16 @@ jobs:
             ${{ runner.os }}-gradle-
       - name: Start synapse server
         run: |
-          python3 -m venv .synapse
-          source .synapse/bin/activate
-          pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-            | sed s/127.0.0.1/0.0.0.0/g | bash
+          git clone -b develop https://github.com/matrix-org/synapse.git
+          cd synapse
+          source env/bin/activate
+          pip install -e .
+          demo/start.sh --no-rate-limit
+#          python3 -m venv .synapse
+#          source .synapse/bin/activate
+#          pip install synapse matrix-synapse
+#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+#            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 3fb6b81505..d84f886896 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -47,7 +47,7 @@ android {
 
     testOptions {
         // Comment to run on Android 12
-        execution 'ANDROIDX_TEST_ORCHESTRATOR'
+//        execution 'ANDROIDX_TEST_ORCHESTRATOR'
     }
 
     buildTypes {
diff --git a/vector/build.gradle b/vector/build.gradle
index 4a26717782..a720b5f988 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -205,7 +205,7 @@ android {
         animationsDisabled = true
 
         // Comment to run on Android 12
-        execution 'ANDROIDX_TEST_ORCHESTRATOR'
+//        execution 'ANDROIDX_TEST_ORCHESTRATOR'
     }
 
     signingConfigs {

From 5edc0506cefafd7ca0632fa7eb8b62b881602708 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:23:00 +0200
Subject: [PATCH 048/130] Github actions improvement test

---
 .github/workflows/integration.yml | 27 ++++++++++++++-------------
 1 file changed, 14 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 5cc8882e20..0faee518f2 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -67,6 +67,7 @@ jobs:
       - name: Start synapse server
         run: |
           git clone -b develop https://github.com/matrix-org/synapse.git
+          ls
           cd synapse
           source env/bin/activate
           pip install -e .
@@ -76,16 +77,16 @@ jobs:
 #          pip install synapse matrix-synapse
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
 #            | sed s/127.0.0.1/0.0.0.0/g | bash
-      - name: Run integration tests on API ${{ matrix.api-level }}
-        uses: reactivecircus/android-emulator-runner@v2
-        with:
-          api-level: ${{ matrix.api-level }}
-          #arch: x86_64
-          #disable-animations: true
-          # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
-          arch: x86
-          profile: Nexus 5X
-          force-avd-creation: false
-          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
-          emulator-build: 7425822
-          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+#      - name: Run integration tests on API ${{ matrix.api-level }}
+#        uses: reactivecircus/android-emulator-runner@v2
+#        with:
+#          api-level: ${{ matrix.api-level }}
+#          #arch: x86_64
+#          #disable-animations: true
+#          # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
+#          arch: x86
+#          profile: Nexus 5X
+#          force-avd-creation: false
+#          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+#          emulator-build: 7425822
+#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace

From 683fcc7f3ed93e4ab01f0db4b989c7b427d54fad Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:26:58 +0200
Subject: [PATCH 049/130] Github actions improvement test

---
 .github/workflows/integration.yml | 24 +++++++++++-------------
 1 file changed, 11 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 0faee518f2..6e7b945336 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -56,22 +56,20 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-pip-
             ${{ runner.os }}-
-      - uses: actions/cache@v2
-        with:
-          path: |
-            ~/.gradle/caches
-            ~/.gradle/wrapper
-          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-          restore-keys: |
-            ${{ runner.os }}-gradle-
+#      - uses: actions/cache@v2
+#        with:
+#          path: |
+#            ~/.gradle/caches
+#            ~/.gradle/wrapper
+#          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+#          restore-keys: |
+#            ${{ runner.os }}-gradle-
       - name: Start synapse server
         run: |
           git clone -b develop https://github.com/matrix-org/synapse.git
-          ls
-          cd synapse
-          source env/bin/activate
-          pip install -e .
-          demo/start.sh --no-rate-limit
+          source .synapse/env/bin/activate
+          pip install -e .synapse
+          .synapse/demo/start.sh --no-rate-limit
 #          python3 -m venv .synapse
 #          source .synapse/bin/activate
 #          pip install synapse matrix-synapse

From 4ef9d089e7335af1a22fce49e4d995831697227b Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:28:51 +0200
Subject: [PATCH 050/130] Github actions improvement test

---
 .github/workflows/integration.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 6e7b945336..1b6f27bcac 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -67,9 +67,9 @@ jobs:
       - name: Start synapse server
         run: |
           git clone -b develop https://github.com/matrix-org/synapse.git
-          source .synapse/env/bin/activate
-          pip install -e .synapse
-          .synapse/demo/start.sh --no-rate-limit
+          source synapse/env/bin/activate
+          pip install -e synapse
+          synapse/demo/start.sh --no-rate-limit
 #          python3 -m venv .synapse
 #          source .synapse/bin/activate
 #          pip install synapse matrix-synapse

From b4d5d1320569e1bfda7ba2f1bbe7194cd3653d3f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:37:22 +0200
Subject: [PATCH 051/130] Github actions improvement test

---
 .github/workflows/integration.yml | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 1b6f27bcac..7011932fd3 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -64,12 +64,15 @@ jobs:
 #          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
 #          restore-keys: |
 #            ${{ runner.os }}-gradle-
+      - uses: actions/checkout@develop
+        with:
+          repository: matrix-org/synapse
       - name: Start synapse server
         run: |
-          git clone -b develop https://github.com/matrix-org/synapse.git
-          source synapse/env/bin/activate
-          pip install -e synapse
-          synapse/demo/start.sh --no-rate-limit
+          cd synapse
+          source env/bin/activate
+          pip install -e .
+          demo/start.sh --no-rate-limit
 #          python3 -m venv .synapse
 #          source .synapse/bin/activate
 #          pip install synapse matrix-synapse

From f7a208800931b7bc4376781480b2970753bb1ecb Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:39:33 +0200
Subject: [PATCH 052/130] Github actions improvement test

---
 .github/workflows/integration.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 7011932fd3..561b96b542 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -64,9 +64,10 @@ jobs:
 #          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
 #          restore-keys: |
 #            ${{ runner.os }}-gradle-
-      - uses: actions/checkout@develop
+      - uses: actions/checkout@v2
         with:
           repository: matrix-org/synapse
+          ref: develop
       - name: Start synapse server
         run: |
           cd synapse

From ae2dbb808f6c9608ff37a16dc1cbc32936465196 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:41:44 +0200
Subject: [PATCH 053/130] Github actions improvement test

---
 .github/workflows/integration.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 561b96b542..97b69f94cd 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -70,7 +70,9 @@ jobs:
           ref: develop
       - name: Start synapse server
         run: |
+          pwd
           cd synapse
+          pwd
           source env/bin/activate
           pip install -e .
           demo/start.sh --no-rate-limit

From 7e3a074f8bd697d8966b67d52ebacf759eb9ffb9 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:45:05 +0200
Subject: [PATCH 054/130] Github actions improvement test

---
 .github/workflows/integration.yml | 58 ++++++++++++++-----------------
 1 file changed, 26 insertions(+), 32 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 97b69f94cd..9de1b2f407 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -56,41 +56,35 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-pip-
             ${{ runner.os }}-
-#      - uses: actions/cache@v2
-#        with:
-#          path: |
-#            ~/.gradle/caches
-#            ~/.gradle/wrapper
-#          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
-#          restore-keys: |
-#            ${{ runner.os }}-gradle-
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.gradle/caches
+            ~/.gradle/wrapper
+          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+          restore-keys: |
+            ${{ runner.os }}-gradle-
       - uses: actions/checkout@v2
         with:
           repository: matrix-org/synapse
           ref: develop
       - name: Start synapse server
         run: |
-          pwd
-          cd synapse
-          pwd
-          source env/bin/activate
-          pip install -e .
-          demo/start.sh --no-rate-limit
-#          python3 -m venv .synapse
-#          source .synapse/bin/activate
-#          pip install synapse matrix-synapse
-#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-#            | sed s/127.0.0.1/0.0.0.0/g | bash
-#      - name: Run integration tests on API ${{ matrix.api-level }}
-#        uses: reactivecircus/android-emulator-runner@v2
-#        with:
-#          api-level: ${{ matrix.api-level }}
-#          #arch: x86_64
-#          #disable-animations: true
-#          # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
-#          arch: x86
-#          profile: Nexus 5X
-#          force-avd-creation: false
-#          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
-#          emulator-build: 7425822
-#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+          python3 -m venv .synapse
+          source .synapse/bin/activate
+          pip install synapse matrix-synapse
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
+            | sed s/127.0.0.1/0.0.0.0/g | bash --no-rate-limit
+      - name: Run integration tests on API ${{ matrix.api-level }}
+        uses: reactivecircus/android-emulator-runner@v2
+        with:
+          api-level: ${{ matrix.api-level }}
+          #arch: x86_64
+          #disable-animations: true
+          # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
+          arch: x86
+          profile: Nexus 5X
+          force-avd-creation: false
+          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+          emulator-build: 7425822
+          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace

From 4d6d9181ab42144fd0edc5523c362af6dfb8fdde Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:52:56 +0200
Subject: [PATCH 055/130] Github actions improvement test

---
 .github/workflows/integration.yml | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 9de1b2f407..d22abc6c92 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -73,8 +73,9 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
-            | sed s/127.0.0.1/0.0.0.0/g | bash --no-rate-limit
+#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+#            | sed s/127.0.0.1/0.0.0.0/g | bash
+          curl -s https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:

From 540687cc4b4be82f22d81e005960b3224f35fd58 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:54:21 +0200
Subject: [PATCH 056/130] Github actions improvement test

---
 .github/workflows/integration.yml | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index d22abc6c92..c757721a9c 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -64,10 +64,6 @@ jobs:
           key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
           restore-keys: |
             ${{ runner.os }}-gradle-
-      - uses: actions/checkout@v2
-        with:
-          repository: matrix-org/synapse
-          ref: develop
       - name: Start synapse server
         run: |
           python3 -m venv .synapse

From c8e4fad5c88a0bb752a825560f53338102e98e59 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:57:19 +0200
Subject: [PATCH 057/130] Github actions improvement test

---
 .github/workflows/integration.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index c757721a9c..c0b6755e41 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -71,7 +71,7 @@ jobs:
           pip install synapse matrix-synapse
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
 #            | sed s/127.0.0.1/0.0.0.0/g | bash
-          curl -s https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:

From bb85e9c0c278aab30d43df543f6079d38d1db885 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:57:54 +0200
Subject: [PATCH 058/130] Github actions improvement test

---
 .github/workflows/integration.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index c0b6755e41..9ec4bbf4cf 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,9 +69,9 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-#            | sed s/127.0.0.1/0.0.0.0/g | bash
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+            | sed s/127.0.0.1/0.0.0.0/g | bash
+#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:

From 1127a2692875dad325a5959544b539314798c92b Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 12:58:52 +0200
Subject: [PATCH 059/130] Github actions improvement test

---
 .github/workflows/integration.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 9ec4bbf4cf..47aa740546 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,8 +69,8 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-            | sed s/127.0.0.1/0.0.0.0/g | bash
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
+            | sed s/127.0.0.1/0.0.0.0/g | bash -s --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2

From 5e282533b6a483d050ccbd04c89fb47152fe68ca Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 13:02:05 +0200
Subject: [PATCH 060/130] Github actions improvement test

---
 .github/workflows/integration.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 47aa740546..d5efa8c8a2 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -70,7 +70,7 @@ jobs:
           source .synapse/bin/activate
           pip install synapse matrix-synapse
           curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
-            | sed s/127.0.0.1/0.0.0.0/g | bash -s --no-rate-limit
+            | sed s/127.0.0.1/0.0.0.0/g | bash -sL --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2

From c14420378bca005c943210257f461e82962890aa Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 13:06:13 +0200
Subject: [PATCH 061/130] Github actions improvement test

---
 .github/workflows/integration.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index d5efa8c8a2..16361c7636 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,8 +69,7 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
-            | sed s/127.0.0.1/0.0.0.0/g | bash -sL --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -sL --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2

From aadbf69f3af24900344b6352e19e79688142681a Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 13:13:46 +0200
Subject: [PATCH 062/130] Github actions improvement test

---
 .github/workflows/integration.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 16361c7636..1f087e1ff1 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,7 +69,7 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -sL --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2

From e482ef4262aef95a9dfa1b9f0ede967032c31e2e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 16:51:12 +0200
Subject: [PATCH 063/130] First local thread integration test

---
 .github/workflows/integration.yml             |   4 +-
 .../android/sdk/common/CommonTestHelper.kt    |  59 +++++++++-
 .../threads/GenerateThreadMessageTests.kt     | 107 ++++++++++++++++++
 .../sdk/session/room/threads/RetryTestRule.kt |  58 ++++++++++
 4 files changed, 224 insertions(+), 4 deletions(-)
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 1f087e1ff1..c18ca69fde 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,8 +69,8 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
-#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 8e21828562..0edd8c52df 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -184,18 +184,73 @@ class CommonTestHelper(context: Context) {
     /**
      * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(room: Room, message: String, count: Int) {
+    private fun sendTextMessagesBatched(room: Room, message: String, count: Int, rootThreadEventId: String? = null) {
         (1 until count + 1)
                 .map { "$message #$it" }
                 .chunked(10)
                 .forEach { batchedMessages ->
                     batchedMessages.forEach { formattedMessage ->
-                        room.sendTextMessage(formattedMessage)
+                        if (rootThreadEventId != null) {
+                            room.replyInThread(
+                                    rootThreadEventId = rootThreadEventId,
+                                    replyInThreadText = formattedMessage)
+                        } else {
+                            room.sendTextMessage(formattedMessage)
+                        }
                     }
                     Thread.sleep(1_000L)
                 }
     }
 
+    /**
+     * Reply in a thread
+     * @param room         the room where to send the messages
+     * @param message      the message to send
+     * @param numberOfMessages the number of time the message will be sent
+     */
+    fun replyInThreadMessage(
+            room: Room,
+            message: String,
+            numberOfMessages: Int,
+            rootThreadEventId: String,
+            timeout: Long = TestConstants.timeOutMillis): List {
+
+        val sentEvents = ArrayList(numberOfMessages)
+        val timeline = room.createTimeline(null, TimelineSettings(10))
+        timeline.start()
+        waitWithLatch(timeout + 1_000L * numberOfMessages) { latch ->
+            val timelineListener = object : Timeline.Listener {
+                override fun onTimelineFailure(throwable: Throwable) {
+                }
+
+                override fun onNewTimelineEvents(eventIds: List) {
+                    // noop
+                }
+
+                override fun onTimelineUpdated(snapshot: List) {
+                    val newMessages = snapshot
+                            .filter { it.root.sendState == SendState.SYNCED }
+                            .filter { it.root.getClearType() == EventType.MESSAGE }
+                            .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true }
+
+                    Timber.v("New synced message size: ${newMessages.size}")
+                    if (newMessages.size == numberOfMessages) {
+                        sentEvents.addAll(newMessages)
+                        // Remove listener now, if not at the next update sendEvents could change
+                        timeline.removeListener(this)
+                        latch.countDown()
+                    }
+                }
+            }
+            timeline.addListener(timelineListener)
+            sendTextMessagesBatched(room, message, numberOfMessages, rootThreadEventId)
+        }
+        timeline.dispose()
+        // Check that all events has been created
+        assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
+        return sentEvents
+    }
+
     // PRIVATE METHODS *****************************************************************************
 
     /**
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
new file mode 100644
index 0000000000..79f5e8314d
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 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.session.room.threads
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldBeTrue
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
+import org.matrix.android.sdk.api.session.events.model.isTextMessage
+import org.matrix.android.sdk.api.session.events.model.isThread
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import timber.log.Timber
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class GenerateThreadMessageTests : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
+    private val logPrefix = "---Test--> "
+
+//    @Rule
+//    @JvmField
+//    val mRetryTestRule = RetryTestRule()
+
+    @Test
+    fun reply_in_thread_to_normal_timeline_message_should_create_a_thread() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue() ?: assert(false)
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                Timber.e("$logPrefix $initMessageThreadDetails")
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
new file mode 100644
index 0000000000..099491655f
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 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.session.room.threads
+
+import android.util.Log
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Retry test rule used to retry test that failed.
+ * Retry failed test 3 times
+ */
+class RetryTestRule(val retryCount: Int = 3) : TestRule {
+
+    private val TAG = RetryTestRule::class.java.simpleName
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return statement(base, description)
+    }
+
+    private fun statement(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                var caughtThrowable: Throwable? = null
+
+                // implement retry logic here
+                for (i in 0 until retryCount) {
+                    try {
+                        base.evaluate()
+                        return
+                    } catch (t: Throwable) {
+                        caughtThrowable = t
+                        Log.e(TAG, description.displayName + ": run " + (i + 1) + " failed")
+                    }
+                }
+
+                Log.e(TAG, description.displayName + ": giving up after " + retryCount + " failures")
+                throw caughtThrowable!!
+            }
+        }
+    }
+}

From b67199eb07707e7f22fd40b67c537f06dd418df4 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 18:12:16 +0200
Subject: [PATCH 064/130] Github actions test

---
 .github/workflows/integration.yml | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index c18ca69fde..5b44bfce32 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,8 +69,13 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-            | sed s/127.0.0.1/0.0.0.0/g | bash
+          pwd
+          cd synapse
+          pwd
+          curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
+          ls
+#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+#            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:

From e7dfdce057f16d45a662d9edde70be7368957b34 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 18:15:41 +0200
Subject: [PATCH 065/130] Github actions test

---
 .github/workflows/integration.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 5b44bfce32..195f7347b0 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -70,7 +70,7 @@ jobs:
           source .synapse/bin/activate
           pip install synapse matrix-synapse
           pwd
-          cd synapse
+          cd .synapse
           pwd
           curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
           ls

From 70d1c15b89ca162532e9ce2ffdf81da1bb5777d7 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 18:21:52 +0200
Subject: [PATCH 066/130] Github actions test

---
 .github/workflows/integration.yml | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 195f7347b0..f27f71eee1 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,11 +69,8 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          pwd
-          cd .synapse
-          pwd
           curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
-          ls
+          ./start.sh --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
 #            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}

From 1b2ce33f7aa6600734db05fe74db3e9771b75260 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 18:24:43 +0200
Subject: [PATCH 067/130] Github actions test

---
 .github/workflows/integration.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index f27f71eee1..0ba0ad904f 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -70,6 +70,7 @@ jobs:
           source .synapse/bin/activate
           pip install synapse matrix-synapse
           curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
+          chmod 777 start.sh
           ./start.sh --no-rate-limit
 #          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
 #            | sed s/127.0.0.1/0.0.0.0/g | bash

From 929cc29f778e020305e6bacc2064e4b35db21bb9 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 19:18:51 +0200
Subject: [PATCH 068/130] Update copyright

---
 .../matrix/android/sdk/session/room/threads/RetryTestRule.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
index 099491655f..c06a18aeb3 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 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.

From 3ef960c4c38e0dc812eb907538f22d97f2da44b7 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 19:45:48 +0200
Subject: [PATCH 069/130] Update copyright

---
 .../sdk/session/room/threads/GenerateThreadMessageTests.kt      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
index 79f5e8314d..e6980c0304 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 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.

From 925c1671a6d47ee3dd9871d25c8dc06d75d677ee Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 3 Jan 2022 21:09:36 +0200
Subject: [PATCH 070/130] Add more integrations tests for threads

---
 .../threads/GenerateThreadMessageTests.kt     | 107 ------
 .../room/threads/ThreadMessagingTest.kt       | 341 ++++++++++++++++++
 2 files changed, 341 insertions(+), 107 deletions(-)
 delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
deleted file mode 100644
index e6980c0304..0000000000
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright 2022 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.session.room.threads
-
-import org.amshove.kluent.shouldBe
-import org.amshove.kluent.shouldBeEqualTo
-import org.amshove.kluent.shouldBeFalse
-import org.amshove.kluent.shouldBeNull
-import org.amshove.kluent.shouldBeTrue
-import org.junit.Assert
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.junit.runners.MethodSorters
-import org.matrix.android.sdk.InstrumentedTest
-import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
-import org.matrix.android.sdk.api.session.events.model.isTextMessage
-import org.matrix.android.sdk.api.session.events.model.isThread
-import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
-import timber.log.Timber
-import java.util.concurrent.CountDownLatch
-
-@RunWith(JUnit4::class)
-@FixMethodOrder(MethodSorters.JVM)
-class GenerateThreadMessageTests : InstrumentedTest {
-
-    private val commonTestHelper = CommonTestHelper(context())
-    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
-    private val logPrefix = "---Test--> "
-
-//    @Rule
-//    @JvmField
-//    val mRetryTestRule = RetryTestRule()
-
-    @Test
-    fun reply_in_thread_to_normal_timeline_message_should_create_a_thread() {
-        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
-
-        val aliceSession = cryptoTestData.firstSession
-        val aliceRoomId = cryptoTestData.roomId
-
-        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
-
-        // Let's send a message in the normal timeline
-        val textMessage = "This is a normal timeline message"
-        val sentMessages = commonTestHelper.sendTextMessage(
-                room = aliceRoom,
-                message = textMessage,
-                nbOfMessages = 1)
-
-        val initMessage = sentMessages.first()
-
-        initMessage.root.isThread().shouldBeFalse()
-        initMessage.root.isTextMessage().shouldBeTrue()
-        initMessage.root.getRootThreadEventId().shouldBeNull()
-        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
-
-        // Let's reply in timeline to that message
-        val repliesInThread = commonTestHelper.replyInThreadMessage(
-                room = aliceRoom,
-                message = "Reply In the above thread",
-                numberOfMessages = 1,
-                rootThreadEventId = initMessage.root.eventId.orEmpty())
-
-        val replyInThread = repliesInThread.first()
-        replyInThread.root.isThread().shouldBeTrue()
-        replyInThread.root.isTextMessage().shouldBeTrue()
-        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
-
-        // The init normal message should now be a root thread event
-        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
-        timeline.start()
-
-        aliceSession.startSync(true)
-        run {
-            val lock = CountDownLatch(1)
-            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
-                initMessageThreadDetails?.isRootThread?.shouldBeTrue() ?: assert(false)
-                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
-                Timber.e("$logPrefix $initMessageThreadDetails")
-                true
-            }
-            timeline.addListener(eventsListener)
-            commonTestHelper.await(lock, 600_000)
-        }
-        aliceSession.stopSync()
-    }
-}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
new file mode 100644
index 0000000000..0fdcfb2870
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2022 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.session.room.threads
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldBeTrue
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
+import org.matrix.android.sdk.api.session.events.model.isTextMessage
+import org.matrix.android.sdk.api.session.events.model.isThread
+import org.matrix.android.sdk.api.session.room.timeline.Timeline
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class ThreadMessagingTest : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
+//    @Rule
+//    @JvmField
+//    val mRetryTestRule = RetryTestRule()
+
+    @Test
+    fun reply_in_thread_should_create_a_thread() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull {
+                    it.root.eventId == initMessage.root.eventId
+                }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+
+    @Test
+    fun reply_in_thread_should_create_a_thread_from_other_user() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message from another user
+        val bobSession = cryptoTestData.secondSession!!
+        val bobRoomId = cryptoTestData.roomId
+        val bobRoom = bobSession.getRoom(bobRoomId)!!
+
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+
+        bobSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        bobSession.stopSync()
+    }
+
+    @Test
+    fun reply_in_thread_to_timeline_message_multiple_times() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send 5 messages in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 5)
+
+        sentMessages.forEach {
+            it.root.isThread().shouldBeFalse()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId().shouldBeNull()
+            it.root.threadDetails?.isRootThread?.shouldBeFalse()
+        }
+        // let's start the thread from the second message
+        val selectedInitMessage = sentMessages[1]
+
+        // Let's reply 40 times in the timeline to the second message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 40,
+                rootThreadEventId = selectedInitMessage.root.eventId.orEmpty())
+
+        repliesInThread.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(selectedInitMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == selectedInitMessage.root.eventId }?.root?.threadDetails
+                // Selected init message should be the thread root
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                // All threads should be 40
+                initMessageThreadDetails?.numberOfThreads?.shouldBeEqualTo(40)
+                true
+            }
+            // Because we sent more than 30 messages we should paginate a bit more
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+
+    @Test
+    fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send 5 messages in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 5)
+
+        sentMessages.forEach {
+            it.root.isThread().shouldBeFalse()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId().shouldBeNull()
+            it.root.threadDetails?.isRootThread?.shouldBeFalse()
+        }
+        // let's start the thread from the second message
+        val firstMessage = sentMessages[0]
+        val secondMessage = sentMessages[1]
+
+
+        // Alice will reply in thread to the second message 35 times
+        val aliceThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Alice reply In the above second thread message",
+                numberOfMessages = 35,
+                rootThreadEventId = secondMessage.root.eventId.orEmpty())
+
+        // Let's reply in timeline to that message from another user
+        val bobSession = cryptoTestData.secondSession!!
+        val bobRoomId = cryptoTestData.roomId
+        val bobRoom = bobSession.getRoom(bobRoomId)!!
+
+        // Bob will reply in thread to the first message 35 times
+        val bobThreadRepliesInFirstMessage = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Bob reply In the above first thread message",
+                numberOfMessages = 42,
+                rootThreadEventId = firstMessage.root.eventId.orEmpty())
+
+        // Bob will also reply in second thread 5 times
+        val bobThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
+                room = bobRoom,
+                message = "Another Bob reply In the above second thread message",
+                numberOfMessages = 20,
+                rootThreadEventId = secondMessage.root.eventId.orEmpty())
+
+
+        aliceThreadRepliesInSecondMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        bobThreadRepliesInFirstMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(firstMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        bobThreadRepliesInSecondMessage.forEach {
+            it.root.isThread().shouldBeTrue()
+            it.root.isTextMessage().shouldBeTrue()
+            it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
+        }
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val firstMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == firstMessage.root.eventId }?.root?.threadDetails
+                val secondMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == secondMessage.root.eventId }?.root?.threadDetails
+
+                // first & second message should be the thread root
+                firstMessageThreadDetails?.isRootThread?.shouldBeTrue()
+                secondMessageThreadDetails?.isRootThread?.shouldBeTrue()
+
+                // First thread message should contain 42
+                firstMessageThreadDetails?.numberOfThreads shouldBeEqualTo 42
+                // Second thread message should contain 35+20
+                secondMessageThreadDetails?.numberOfThreads shouldBeEqualTo 55
+
+                true
+            }
+            // Because we sent more than 30 messages we should paginate a bit more
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.paginate(Timeline.Direction.BACKWARDS, 50)
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+
+}

From 42002b80edc6200127c553752fec832970565969 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:23:34 +0200
Subject: [PATCH 071/130] Github actions test

---
 .github/workflows/integration.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 0ba0ad904f..7504d825c5 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -86,4 +86,5 @@ jobs:
           force-avd-creation: false
           emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
           emulator-build: 7425822
-          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
+#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace

From da8ec4debf2cc0d790dcfc5bf176bd0149719f32 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:25:52 +0200
Subject: [PATCH 072/130] Github actions test

---
 .github/workflows/integration.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 7504d825c5..3fac485fee 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -72,8 +72,6 @@ jobs:
           curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
           chmod 777 start.sh
           ./start.sh --no-rate-limit
-#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
-#            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:

From 948f75b215dfd2a2900fe4b7994e0282e41fdbf4 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:30:51 +0200
Subject: [PATCH 073/130] Github actions test

---
 .github/workflows/integration.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 3fac485fee..598ba19bc9 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -84,5 +84,6 @@ jobs:
           force-avd-creation: false
           emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
           emulator-build: 7425822
-          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
 #          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
+

From ef2c32e2c92af6bc0be3f9ecf32e7af2256d8082 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:32:39 +0200
Subject: [PATCH 074/130] Github actions test

---
 .../android/sdk/session/room/threads/ThreadMessagingTest.kt      | 1 +
 1 file changed, 1 insertion(+)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index 0fdcfb2870..0d2458c6d2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -100,6 +100,7 @@ class ThreadMessagingTest : InstrumentedTest {
             timeline.addListener(eventsListener)
             commonTestHelper.await(lock, 600_000)
         }
+
         aliceSession.stopSync()
     }
 

From 84c537315c883f9ab0f9f13e44f0bb7724d13497 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:38:50 +0200
Subject: [PATCH 075/130] Github actions test

---
 .../android/sdk/session/room/threads/ThreadMessagingTest.kt      | 1 -
 1 file changed, 1 deletion(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index 0d2458c6d2..0fdcfb2870 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -100,7 +100,6 @@ class ThreadMessagingTest : InstrumentedTest {
             timeline.addListener(eventsListener)
             commonTestHelper.await(lock, 600_000)
         }
-
         aliceSession.stopSync()
     }
 

From ddfdf180c29da816252826d5542f6e4e75d176fb Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:45:57 +0200
Subject: [PATCH 076/130] Github actions test

---
 .github/workflows/integration.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 598ba19bc9..f2559565f1 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -84,6 +84,6 @@ jobs:
           force-avd-creation: false
           emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
           emulator-build: 7425822
-#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
-          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
+          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
 

From 91bda140e96594327a0ff955ac2e7dc3b07dd997 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:46:23 +0200
Subject: [PATCH 077/130] Github actions test

---
 .github/workflows/integration.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index f2559565f1..598ba19bc9 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -84,6 +84,6 @@ jobs:
           force-avd-creation: false
           emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
           emulator-build: 7425822
-          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
-#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
+#          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
+          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
 

From f1f1d59576da01da5d53aa36bb4ea0afe9e889fb Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 4 Jan 2022 00:49:39 +0200
Subject: [PATCH 078/130] Github actions test

---
 .github/workflows/integration.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 598ba19bc9..8f208f0fe7 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -87,3 +87,4 @@ jobs:
 #          script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace
           script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.threads.ThreadMessagingTest matrix-sdk-android:connectedAndroidTest --info
 
+

From ae81f61958d4e6d52fe9b549efad513143bb5496 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 7 Jan 2022 16:28:58 +0200
Subject: [PATCH 079/130] fix integration test

---
 .../android/sdk/common/CommonTestHelper.kt    | 32 ++-----------------
 1 file changed, 2 insertions(+), 30 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index da658fdf7d..9f9667a759 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -157,7 +157,7 @@ class CommonTestHelper(context: Context) {
     /**
      * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long,rootThreadEventId: String? = null): List {
+    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List {
         val sentEvents = ArrayList(count)
         (1 until count + 1)
                 .map { "$message #$it" }
@@ -214,37 +214,9 @@ class CommonTestHelper(context: Context) {
             numberOfMessages: Int,
             rootThreadEventId: String,
             timeout: Long = TestConstants.timeOutMillis): List {
-
-        val sentEvents = ArrayList(numberOfMessages)
         val timeline = room.createTimeline(null, TimelineSettings(10))
         timeline.start()
-        waitWithLatch(timeout + 1_000L * numberOfMessages) { latch ->
-            val timelineListener = object : Timeline.Listener {
-                override fun onTimelineFailure(throwable: Throwable) {
-                }
-
-                override fun onNewTimelineEvents(eventIds: List) {
-                    // noop
-                }
-
-                override fun onTimelineUpdated(snapshot: List) {
-                    val newMessages = snapshot
-                            .filter { it.root.sendState == SendState.SYNCED }
-                            .filter { it.root.getClearType() == EventType.MESSAGE }
-                            .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true }
-
-                    Timber.v("New synced message size: ${newMessages.size}")
-                    if (newMessages.size == numberOfMessages) {
-                        sentEvents.addAll(newMessages)
-                        // Remove listener now, if not at the next update sendEvents could change
-                        timeline.removeListener(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            timeline.addListener(timelineListener)
-            sendTextMessagesBatched(room, message, numberOfMessages, rootThreadEventId)
-        }
+        val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout,rootThreadEventId)
         timeline.dispose()
         // Check that all events has been created
         assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())

From e54163680273deaa41da0fc511913913e69be328 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 10 Jan 2022 11:20:31 +0200
Subject: [PATCH 080/130] Make TimelineSettings aware of rootThreadEventId and
 welcome a new Thread mode for the timeline creation

---
 .../session/room/timeline/TimelineSettings.kt | 13 ++++++--
 .../session/room/timeline/DefaultTimeline.kt  | 16 +++++-----
 .../session/room/timeline/LoadMoreResult.kt   |  1 +
 .../room/timeline/LoadTimelineStrategy.kt     | 31 +++++++++++++++----
 .../session/room/timeline/TimelineChunk.kt    | 21 ++++++++++++-
 .../home/room/detail/RoomDetailViewModel.kt   |  2 +-
 .../timeline/factory/TimelineFactory.kt       | 10 ++++--
 .../helper/TimelineSettingsFactory.kt         |  6 ++--
 8 files changed, 79 insertions(+), 21 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
index ceffedb234..6548453c8a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
@@ -27,5 +27,14 @@ data class TimelineSettings(
         /**
          * If true, will build read receipts for each event.
          */
-        val buildReadReceipts: Boolean = true
-)
+        val buildReadReceipts: Boolean = true,
+        /**
+         * The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
+         */
+        val rootThreadEventId: String? = null) {
+
+    /**
+     * Returns true if this is a thread timeline or false otherwise
+     */
+    fun isThreadTimeline() = rootThreadEventId != null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 483851ebd7..03ea2fdcb5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -136,7 +136,7 @@ internal class DefaultTimeline(private val roomId: String,
                     ensureReadReceiptAreLoaded(realm)
                     backgroundRealm.set(realm)
                     listenToPostSnapshotSignals()
-                    openAround(initialEventId)
+                    openAround(initialEventId, rootThreadEventId)
                     postSnapshot()
                 }
             }
@@ -157,7 +157,7 @@ internal class DefaultTimeline(private val roomId: String,
 
     override fun restartWithEventId(eventId: String?) {
         timelineScope.launch {
-            openAround(eventId)
+            openAround(eventId,rootThreadEventId)
             postSnapshot()
         }
     }
@@ -226,18 +226,20 @@ internal class DefaultTimeline(private val roomId: String,
         return true
     }
 
-    private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) {
+    private suspend fun openAround(eventId: String?, rootThreadEventId: String?) = withContext(timelineDispatcher) {
         val baseLogMessage = "openAround(eventId: $eventId)"
         Timber.v("$baseLogMessage started")
         if (!isStarted.get()) {
             throw IllegalStateException("You should call start before using timeline")
         }
         strategy.onStop()
-        strategy = if (eventId == null) {
-            buildStrategy(LoadTimelineStrategy.Mode.Live)
-        } else {
-            buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
+
+        strategy = when {
+            rootThreadEventId != null -> buildStrategy(LoadTimelineStrategy.Mode.Thread(rootThreadEventId))
+            eventId == null           -> buildStrategy(LoadTimelineStrategy.Mode.Live)
+            else                      -> buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
         }
+
         initPaginationStates(eventId)
         strategy.onStart()
         loadMore(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
index c419e8325e..2949e35bd3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
@@ -20,4 +20,5 @@ internal enum class LoadMoreResult {
     REACHED_END,
     SUCCESS,
     FAILURE
+    // evenIDS
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index 528b564e8b..4aee1b1a30 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -51,6 +51,7 @@ internal class LoadTimelineStrategy(
     sealed interface Mode {
         object Live : Mode
         data class Permalink(val originEventId: String) : Mode
+        data class Thread(val rootThreadEventId: String) : Mode
 
         fun originEventId(): String? {
             return if (this is Permalink) {
@@ -59,6 +60,14 @@ internal class LoadTimelineStrategy(
                 null
             }
         }
+
+//        fun getRootThreadEventId(): String? {
+//            return if (this is Thread) {
+//                rootThreadEventId
+//            } else {
+//                null
+//            }
+//        }
     }
 
     data class Dependencies(
@@ -162,6 +171,7 @@ internal class LoadTimelineStrategy(
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
+        ///
         if (mode is Mode.Permalink && timelineChunk == null) {
             val params = GetContextOfEventTask.Params(roomId, mode.originEventId)
             try {
@@ -198,12 +208,21 @@ internal class LoadTimelineStrategy(
     }
 
     private fun getChunkEntity(realm: Realm): RealmResults {
-        return if (mode is Mode.Permalink) {
-            ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
-        } else {
-            ChunkEntity.where(realm, roomId)
-                    .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
-                    .findAll()
+
+        return when (mode) {
+            is Mode.Live      -> {
+                ChunkEntity.where(realm, roomId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .findAll()
+            }
+            is Mode.Permalink -> {
+                ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
+            }
+            is Mode.Thread    -> {
+                ChunkEntity.where(realm, roomId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .findAll()
+            }
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 14cba2a4b8..e05def3805 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -23,6 +23,7 @@ import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.Sort
 import kotlinx.coroutines.CompletableDeferred
+import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -271,7 +272,24 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
     private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int {
         val displayIndex = getNextDisplayIndex(direction) ?: return 0
         val baseQuery = timelineEventEntities.where()
-        val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty()
+
+        val timelineEvents = if (timelineSettings.rootThreadEventId != null) {
+            baseQuery
+                    .beginGroup()
+                    .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, timelineSettings.rootThreadEventId)
+                    .or()
+                    .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, timelineSettings.rootThreadEventId)
+                    .endGroup()
+                    .offsets(direction, count, displayIndex)
+                    .findAll()
+                    .orEmpty()
+        } else {
+            baseQuery
+                    .offsets(direction, count, displayIndex)
+                    .findAll()
+                    .orEmpty()
+        }
+
         if (timelineEvents.isEmpty()) return 0
         fetchRootThreadEventsIfNeeded(timelineEvents)
         if (direction == Timeline.Direction.FORWARDS) {
@@ -299,6 +317,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * in order to be able to display the event to the user appropriately
      */
     private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List) {
+        if (BuildConfig.THREADING_ENABLED) return
         val eventEntityList = offsetResults
                 .mapNotNull {
                     it.root
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index f60df814e5..4ee628ff16 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -127,7 +127,7 @@ class RoomDetailViewModel @AssistedInject constructor(
     private val invisibleEventsSource = BehaviorDataSource()
     private val visibleEventsSource = BehaviorDataSource()
     private var timelineEvents = MutableSharedFlow>(0)
-    val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId)
+    val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId)
 
     // Same lifecycle than the ViewModel (survive to screen rotation)
     val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt
index b57e39b3cf..3ec1366131 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineFactory.kt
@@ -35,8 +35,14 @@ private val secondaryTimelineAllowedTypes = listOf(
 
 class TimelineFactory @Inject constructor(private val session: Session, private val timelineSettingsFactory: TimelineSettingsFactory) {
 
-    fun createTimeline(coroutineScope: CoroutineScope, mainRoom: Room, eventId: String?): Timeline {
-        val settings = timelineSettingsFactory.create()
+    fun createTimeline(
+            coroutineScope: CoroutineScope,
+            mainRoom: Room,
+            eventId: String?,
+            rootThreadEventId: String?
+    ): Timeline {
+        val settings = timelineSettingsFactory.create(rootThreadEventId)
+
         if (!session.vectorCallService.protocolChecker.supportVirtualRooms) {
             return mainRoom.createTimeline(eventId, settings)
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
index 3aee65bf19..8b7dcc9c72 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
@@ -22,9 +22,11 @@ import javax.inject.Inject
 
 class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
 
-    fun create(): TimelineSettings {
+    fun create(rootThreadEventId: String?): TimelineSettings {
         return TimelineSettings(
                 initialSize = 30,
-                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
+                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts(),
+                rootThreadEventId = rootThreadEventId
+        )
     }
 }

From 1b41a72e72166e4750d3612470f715ee89e2f81f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 10 Jan 2022 14:14:11 +0200
Subject: [PATCH 081/130] Fix Quote from within a thread

---
 .../sdk/api/session/room/send/SendService.kt  |  2 +-
 .../session/room/send/DefaultSendService.kt   | 10 +++-
 .../room/send/LocalEchoEventFactory.kt        | 17 ++++++-
 .../composer/MessageComposerViewModel.kt      | 51 +++++--------------
 4 files changed, 37 insertions(+), 43 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index a132d9ff10..f9a775589c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -63,7 +63,7 @@ interface SendService {
      * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
      * @return a [Cancelable]
      */
-    fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable
+    fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable
 
     /**
      * Method to send a media asynchronously.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 14b66b5377..23322b081c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -97,8 +97,14 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable {
-        return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown)
+    override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable {
+        return localEchoEventFactory.createQuotedTextEvent(
+                roomId = roomId,
+                quotedEvent = quotedEvent,
+                text = text,
+                autoMarkdown = autoMarkdown,
+                rootThreadEventId = rootThreadEventId
+        )
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
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 930327b41f..86da186222 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
@@ -476,6 +476,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 newBodyFormatted
         )
     }
+
     private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
         return buildString {
             append("> <")
@@ -564,11 +565,25 @@ internal class LocalEchoEventFactory @Inject constructor(
             quotedEvent: TimelineEvent,
             text: String,
             autoMarkdown: Boolean,
+            rootThreadEventId: String?
     ): Event {
         val messageContent = quotedEvent.getLastMessageContent()
         val textMsg = messageContent?.body
         val quoteText = legacyRiotQuoteText(textMsg, text)
-        return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT)
+
+        return if (rootThreadEventId != null) {
+            createMessageEvent(
+                    roomId,
+                    markdownParser
+                            .parse(quoteText, force = true, advanced = autoMarkdown)
+                            .toThreadTextContent(rootThreadEventId, MessageType.MSGTYPE_TEXT)
+            )
+        } else {
+            createFormattedTextEvent(
+                    roomId,
+                    markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
+                    MessageType.MSGTYPE_TEXT)
+        }
     }
 
     private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index ea89313131..c15c026e9d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -242,7 +242,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             handleJoinToAnotherRoomSlashCommand(slashCommandResult)
                             popDraft()
                         }
-                        is ParsedCommand.PartRoom                 -> {
+                        is ParsedCommand.PartRoom                          -> {
                             handlePartSlashCommand(slashCommandResult)
                         }
                         is ParsedCommand.SendEmote                         -> {
@@ -325,7 +325,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                         is ParsedCommand.ChangeAvatarForRoom               -> {
                             handleChangeAvatarForRoomSlashCommand(slashCommandResult)
                         }
-                        is ParsedCommand.ShowUser                 -> {
+                        is ParsedCommand.ShowUser                          -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk())
                             handleWhoisSlashCommand(slashCommandResult)
                             popDraft()
@@ -343,7 +343,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 )
                             }
                         }
-                        is ParsedCommand.CreateSpace              -> {
+                        is ParsedCommand.CreateSpace                       -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
@@ -367,7 +367,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
                             Unit
                         }
-                        is ParsedCommand.AddToSpace               -> {
+                        is ParsedCommand.AddToSpace                        -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
@@ -386,7 +386,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
                             Unit
                         }
-                        is ParsedCommand.JoinSpace                -> {
+                        is ParsedCommand.JoinSpace                         -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
@@ -399,7 +399,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
                             Unit
                         }
-                        is ParsedCommand.LeaveRoom                -> {
+                        is ParsedCommand.LeaveRoom                         -> {
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
                                     session.getRoom(slashCommandResult.roomId)?.leave(null)
@@ -411,7 +411,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
                             Unit
                         }
-                        is ParsedCommand.UpgradeRoom              -> {
+                        is ParsedCommand.UpgradeRoom                       -> {
                             _viewEvents.post(
                                     MessageComposerViewEvents.ShowRoomUpgradeDialog(
                                             slashCommandResult.newVersion,
@@ -446,39 +446,12 @@ class MessageComposerViewModel @AssistedInject constructor(
                     _viewEvents.post(MessageComposerViewEvents.MessageSent)
                     popDraft()
                 }
-//                is SendMode.Quote   -> {
-//                    val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
-//                    val textMsg = messageContent?.body
-//
-//                    val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
-//
-//                    // TODO check for pills?
-//
-//                    // TODO Refactor this, just temporary for quotes
-//                    val parser = Parser.builder().build()
-//                    val document = parser.parse(finalText)
-//                    val renderer = HtmlRenderer.builder().build()
-//                    val htmlText = renderer.render(document)
-//
-//                    if (finalText == htmlText) {
-//                        state.rootThreadEventId?.let {
-//                            room.replyInThread(
-//                                    rootThreadEventId = it,
-//                                    replyInThreadText = finalText)
-//                        } ?: room.sendTextMessage(finalText)
-//                    } else {
-//                        state.rootThreadEventId?.let {
-//                            room.replyInThread(
-//                                    rootThreadEventId = it,
-//                                    replyInThreadText = finalText,
-//                                    formattedText = htmlText)
-//                        } ?: room.sendFormattedTextMessage(finalText, htmlText)
-//                    }
-//                    _viewEvents.post(MessageComposerViewEvents.MessageSent)
-//                    popDraft()
-//                }
                 is SendMode.Quote   -> {
-                    room.sendQuotedTextMessage(state.sendMode.timelineEvent, action.text.toString(), action.autoMarkdown)
+                    room.sendQuotedTextMessage(
+                            quotedEvent = state.sendMode.timelineEvent,
+                            text = action.text.toString(),
+                            autoMarkdown = action.autoMarkdown,
+                            rootThreadEventId = state.rootThreadEventId)
                     _viewEvents.post(MessageComposerViewEvents.MessageSent)
                     popDraft()
                 }

From 37ec3fdf841b1ae9e853005dc01635c0307a3817 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 11 Jan 2022 12:13:53 +0200
Subject: [PATCH 082/130] Refactor threads to support the new timeline
 implementation

---
 .../session/room/timeline/TimelineChunk.kt    | 100 +++++++++++++-----
 .../room/timeline/TokenChunkEventPersistor.kt |   1 -
 2 files changed, 73 insertions(+), 28 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index e05def3805..2f53c666bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -93,7 +93,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 handleDatabaseChangeSet(frozenResults, changeSet)
             }
 
-    private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents()
+    private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents(timelineSettings.rootThreadEventId)
     private val builtEvents: MutableList = Collections.synchronizedList(ArrayList())
     private val builtEventsIndexes: MutableMap = Collections.synchronizedMap(HashMap())
 
@@ -138,13 +138,18 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) {
             return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
         }
-        val loadFromStorageCount = loadFromStorage(count, direction)
-        Timber.v("Has loaded $loadFromStorageCount items from storage in $direction")
-        val offsetCount = count - loadFromStorageCount
+        val loadFromStorage = loadFromStorage(count, direction).also{
+            logLoadedFromStorage(it,direction)
+        }
+
+        val offsetCount = count - loadFromStorage.numberOfEvents
+
         return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
             LoadMoreResult.REACHED_END
         } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
             LoadMoreResult.REACHED_END
+        } else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
+            LoadMoreResult.REACHED_END
         } else if (offsetCount == 0) {
             LoadMoreResult.SUCCESS
         } else {
@@ -188,6 +193,15 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    /**
+     * Simple log that displays the number and timeline of loaded events
+     */
+    private fun logLoadedFromStorage(loadedFromStorage: LoadedFromStorage, direction: Timeline.Direction) =
+        Timber.v("[" +
+                "${if (timelineSettings.isThreadTimeline()) "ThreadTimeLine" else "Timeline"}] Has loaded " +
+                "${loadedFromStorage.numberOfEvents} items from storage in $direction " +
+                if (timelineSettings.isThreadTimeline() && loadedFromStorage.threadReachedEnd) "[Reached End]" else "")
+
     fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
         val builtEventIndex = builtEventsIndexes[eventId]
         if (builtEventIndex != null) {
@@ -268,29 +282,31 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
 
     /**
      * This method tries to read events from the current chunk.
+     * @return the number of events loaded. If we are in a thread timeline it also returns
+     * whether or not we reached the end/root message
      */
-    private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int {
-        val displayIndex = getNextDisplayIndex(direction) ?: return 0
+    private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): LoadedFromStorage {
+        val displayIndex = getNextDisplayIndex(direction) ?: return LoadedFromStorage()
         val baseQuery = timelineEventEntities.where()
 
-        val timelineEvents = if (timelineSettings.rootThreadEventId != null) {
-            baseQuery
-                    .beginGroup()
-                    .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, timelineSettings.rootThreadEventId)
-                    .or()
-                    .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, timelineSettings.rootThreadEventId)
-                    .endGroup()
-                    .offsets(direction, count, displayIndex)
-                    .findAll()
-                    .orEmpty()
-        } else {
-            baseQuery
-                    .offsets(direction, count, displayIndex)
-                    .findAll()
-                    .orEmpty()
-        }
+//        val timelineEvents = if (timelineSettings.rootThreadEventId != null) {
+//            baseQuery
+//                    .beginGroup()
+//                    .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, timelineSettings.rootThreadEventId)
+//                    .or()
+//                    .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, timelineSettings.rootThreadEventId)
+//                    .endGroup()
+//                    .offsets(direction, count, displayIndex)
+//                    .findAll()
+//                    .orEmpty()
+//        } else {
+        val timelineEvents = baseQuery
+                .offsets(direction, count, displayIndex)
+                .findAll()
+                .orEmpty()
+//        }
 
-        if (timelineEvents.isEmpty()) return 0
+        if (timelineEvents.isEmpty()) return LoadedFromStorage()
         fetchRootThreadEventsIfNeeded(timelineEvents)
         if (direction == Timeline.Direction.FORWARDS) {
             builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
@@ -309,9 +325,20 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                         builtEvents.add(timelineEvent)
                     }
                 }
-        return timelineEvents.size
+        return LoadedFromStorage(
+                threadReachedEnd = threadReachedEnd(timelineEvents),
+                numberOfEvents = timelineEvents.size)
     }
 
+    /**
+     * Returns whether or not the the thread has reached end. It returned false if the current timeline
+     * is not a thread timeline
+     */
+    private fun threadReachedEnd(timelineEvents: List): Boolean =
+            timelineSettings.rootThreadEventId?.let { rootThreadId ->
+                timelineEvents.firstOrNull { it.eventId == rootThreadId }?.let { true }
+            } ?: false
+
     /**
      * This function is responsible to fetch and store the root event of a thread event
      * in order to be able to display the event to the user appropriately
@@ -362,7 +389,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val loadMoreResult = try {
             if (token == null) {
                 if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END
-                val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE
+                val lastKnownEventId = chunkEntity.sortedTimelineEvents(timelineSettings.rootThreadEventId).firstOrNull()?.eventId
+                        ?: return LoadMoreResult.FAILURE
                 val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count)
                 fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult()
             } else {
@@ -473,6 +501,11 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 onBuiltEvents = this.onBuiltEvents
         )
     }
+
+    private data class LoadedFromStorage(
+            val threadReachedEnd: Boolean = false,
+            val numberOfEvents: Int = 0
+    )
 }
 
 private fun RealmQuery.offsets(
@@ -493,6 +526,19 @@ private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
     return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
 }
 
-private fun ChunkEntity.sortedTimelineEvents(): RealmResults {
-    return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmResults {
+    return if (rootThreadEventId == null) {
+        timelineEvents
+                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+    } else {
+        timelineEvents
+                .where()
+                .beginGroup()
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .or()
+                .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
+                .endGroup()
+                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+                .findAll()
+    }
 }
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 19d6a1bd28..2641d971b1 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
@@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addStateEvent
 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.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity

From 753e3e75192b2f3a0a26726e5e929174fbb26560 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 11 Jan 2022 15:31:21 +0200
Subject: [PATCH 083/130] - fix ktlint format - Update Threads toolbar UI

---
 .../android/sdk/common/CommonTestHelper.kt       |  2 +-
 .../session/room/threads/ThreadMessagingTest.kt  |  3 ---
 .../session/room/timeline/DefaultTimeline.kt     |  5 ++---
 .../room/timeline/LoadTimelineStrategy.kt        |  3 +--
 .../session/room/timeline/TimelineChunk.kt       |  4 ++--
 .../room/timeline/TokenChunkEventPersistor.kt    |  2 +-
 .../home/room/detail/TimelineFragment.kt         |  1 -
 .../layout/view_room_detail_thread_toolbar.xml   | 16 +++++++++-------
 8 files changed, 16 insertions(+), 20 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 9f9667a759..031d0a8bcf 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -216,7 +216,7 @@ class CommonTestHelper(context: Context) {
             timeout: Long = TestConstants.timeOutMillis): List {
         val timeline = room.createTimeline(null, TimelineSettings(10))
         timeline.start()
-        val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout,rootThreadEventId)
+        val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout, rootThreadEventId)
         timeline.dispose()
         // Check that all events has been created
         assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index 0fdcfb2870..62887beba7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -261,7 +261,6 @@ class ThreadMessagingTest : InstrumentedTest {
         val firstMessage = sentMessages[0]
         val secondMessage = sentMessages[1]
 
-
         // Alice will reply in thread to the second message 35 times
         val aliceThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
                 room = aliceRoom,
@@ -288,7 +287,6 @@ class ThreadMessagingTest : InstrumentedTest {
                 numberOfMessages = 20,
                 rootThreadEventId = secondMessage.root.eventId.orEmpty())
 
-
         aliceThreadRepliesInSecondMessage.forEach {
             it.root.isThread().shouldBeTrue()
             it.root.isTextMessage().shouldBeTrue()
@@ -337,5 +335,4 @@ class ThreadMessagingTest : InstrumentedTest {
         }
         aliceSession.stopSync()
     }
-
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 03ea2fdcb5..037edbd83d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -127,11 +127,10 @@ internal class DefaultTimeline(private val roomId: String,
         }
         timelineScope.launch {
             sequencer.post {
-
                 if (isStarted.compareAndSet(false, true)) {
                     isFromThreadTimeline = rootThreadEventId != null
                     this@DefaultTimeline.rootThreadEventId = rootThreadEventId
-                    ///
+                    // /
                     val realm = Realm.getInstance(realmConfiguration)
                     ensureReadReceiptAreLoaded(realm)
                     backgroundRealm.set(realm)
@@ -157,7 +156,7 @@ internal class DefaultTimeline(private val roomId: String,
 
     override fun restartWithEventId(eventId: String?) {
         timelineScope.launch {
-            openAround(eventId,rootThreadEventId)
+            openAround(eventId, rootThreadEventId)
             postSnapshot()
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index 4aee1b1a30..1d92d89a3a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -171,7 +171,7 @@ internal class LoadTimelineStrategy(
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
-        ///
+        // /
         if (mode is Mode.Permalink && timelineChunk == null) {
             val params = GetContextOfEventTask.Params(roomId, mode.originEventId)
             try {
@@ -208,7 +208,6 @@ internal class LoadTimelineStrategy(
     }
 
     private fun getChunkEntity(realm: Realm): RealmResults {
-
         return when (mode) {
             is Mode.Live      -> {
                 ChunkEntity.where(realm, roomId)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 2f53c666bc..eac653c72d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -138,8 +138,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) {
             return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
         }
-        val loadFromStorage = loadFromStorage(count, direction).also{
-            logLoadedFromStorage(it,direction)
+        val loadFromStorage = loadFromStorage(count, direction).also {
+            logLoadedFromStorage(it, direction)
         }
 
         val offsetCount = count - loadFromStorage.numberOfEvents
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 186c59a562..5115c852ca 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
@@ -50,7 +50,7 @@ import javax.inject.Inject
 internal class TokenChunkEventPersistor @Inject constructor(
                                                             @SessionDatabase private val monarchy: Monarchy,
                                                             @UserId private val userId: String,
-                                                            private val liveEventManager: Lazy ) {
+                                                            private val liveEventManager: Lazy) {
 
     enum class Result {
         SHOULD_FETCH_MORE,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 86e7a5f78d..dc5eb34e08 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -196,7 +196,6 @@ import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import kotlinx.parcelize.Parcelize
 import nl.dionsegijn.konfetti.models.Shape
 import nl.dionsegijn.konfetti.models.Size
 import org.billcarsonfr.jsonviewer.JSonViewerDialog
diff --git a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
index 389dab4936..5de8ec3efa 100644
--- a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
+++ b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
@@ -6,6 +6,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:visibility="gone"
+    tools:layout_height="?actionBarSize"
     tools:visibility="visible">
 
     
 
     
 
     
 

From 1e2fb88783c54877213e6e2f9b1f7a10ee1e7490 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 11 Jan 2022 16:30:29 +0200
Subject: [PATCH 084/130] - fix lint error

---
 .github/workflows/quality.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 483e8819f3..7eff52cc9f 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -74,7 +74,7 @@ jobs:
           edit-mode: replace
       - name: Delete comment if needed
         if: always() && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
-        uses: actions/github-script@v5.1.0
+        uses: actions/github-script@v3
         with:
           script: |
             github.issues.deleteComment({

From 4560d748d3837f39aabd3a9ce5ae891401b68666 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 11 Jan 2022 17:52:14 +0200
Subject: [PATCH 085/130] Display encrypted messages in thread summary and in
 thread list

---
 .../matrix/android/sdk/api/session/events/model/Event.kt  | 4 ++--
 .../android/sdk/internal/database/mapper/EventMapper.kt   | 2 +-
 .../app/features/home/room/detail/TimelineFragment.kt     | 6 +++++-
 .../room/detail/timeline/factory/EncryptedItemFactory.kt  | 7 ++++++-
 .../timeline/helper/MessageItemAttributesFactory.kt       | 4 ++++
 .../home/room/detail/timeline/item/AbsMessageItem.kt      | 3 ++-
 .../room/threads/list/viewmodel/ThreadListController.kt   | 8 ++++++--
 7 files changed, 26 insertions(+), 8 deletions(-)

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 7372a83873..895280732e 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
@@ -197,8 +197,8 @@ data class Event(
      * It can be used especially for message summaries.
      * It will return a decrypted text message or an empty string otherwise.
      */
-    fun getDecryptedTextSummary(): String {
-        val text = getDecryptedValue().orEmpty()
+    fun getDecryptedTextSummary(): String? {
+        val text = getDecryptedValue() ?: return null
         return when {
             isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage()        -> "sent a file."
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 05070efe1f..3504284427 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
@@ -113,7 +113,7 @@ internal object EventMapper {
                         )
                     },
                     threadNotificationState = eventEntity.threadNotificationState,
-                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty(),
+                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
                     lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
 
             )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index dc5eb34e08..7f44aed15b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1902,7 +1902,11 @@ class TimelineFragment @Inject constructor(
                 roomDetailViewModel.handle(action)
             }
             is EncryptedEventContent             -> {
-                roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
+                if(isRootThreadEvent){
+                    onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
+                }else {
+                    roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
+                }
             }
             else                                 -> {
                 onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
index 89c9c51f0c..f1ffc77a36 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
@@ -106,7 +106,12 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
                 }
 
                 val informationData = messageInformationDataFactory.create(params)
-                val attributes = attributesFactory.create(event.root.content.toModel(), informationData, params.callback)
+                val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails
+                val attributes = attributesFactory.create(
+                        messageContent = event.root.content.toModel(),
+                        informationData = informationData,
+                        callback = params.callback,
+                        threadDetails = threadDetails)
                 return MessageTextItem_()
                         .leftGuideline(avatarSizeProvider.leftGuideline)
                         .highlighted(params.isHighlighted)
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 8cc5ffe1ee..a2cdbec7c6 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
@@ -16,6 +16,8 @@
 package im.vector.app.features.home.room.detail.timeline.helper
 
 import im.vector.app.EmojiCompatFontProvider
+import im.vector.app.R
+import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@@ -28,6 +30,7 @@ class MessageItemAttributesFactory @Inject constructor(
         private val avatarRenderer: AvatarRenderer,
         private val messageColorProvider: MessageColorProvider,
         private val avatarSizeProvider: AvatarSizeProvider,
+        private val stringProvider: StringProvider,
         private val emojiCompatFontProvider: EmojiCompatFontProvider) {
 
     fun create(messageContent: Any?,
@@ -53,6 +56,7 @@ class MessageItemAttributesFactory @Inject constructor(
                 threadCallback = callback,
                 readReceiptsCallback = callback,
                 emojiTypeFace = emojiCompatFontProvider.typeface,
+                decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message),
                 threadDetails = threadDetails
         )
     }
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 f75df30916..33eae89e31 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
@@ -118,7 +118,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
             attributes.threadDetails?.let { threadDetails ->
                 holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
                 holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
-                holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage
+                holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage ?: attributes.decryptionErrorMessage
 
                 val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let
                 val displayName = threadDetails.threadSummarySenderInfo?.displayName
@@ -185,6 +185,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
             val threadCallback: TimelineEventController.ThreadCallback? = null,
             override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
             val emojiTypeFace: Typeface? = null,
+            val decryptionErrorMessage: String? = null,
             val threadDetails: ThreadDetails? = null
     ) : AbsBaseMessageItem.Attributes {
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 3f69701a31..984c8e8f7e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -17,8 +17,10 @@
 package im.vector.app.features.home.room.threads.list.viewmodel
 
 import com.airbnb.epoxy.EpoxyController
+import im.vector.app.R
 import im.vector.app.core.date.DateFormatKind
 import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.threads.list.model.threadList
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -28,6 +30,7 @@ import javax.inject.Inject
 
 class ThreadListController @Inject constructor(
         private val avatarRenderer: AvatarRenderer,
+        private val stringProvider: StringProvider,
         private val dateFormatter: VectorDateFormatter
 ) : EpoxyController() {
 
@@ -56,6 +59,7 @@ class ThreadListController @Inject constructor(
                 }
                 ?.forEach { timelineEvent ->
                     val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST)
+                    val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
                     threadList {
                         id(timelineEvent.eventId)
                         avatarRenderer(host.avatarRenderer)
@@ -64,8 +68,8 @@ class ThreadListController @Inject constructor(
                         date(date)
                         rootMessageDeleted(timelineEvent.root.isRedacted())
                         threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
-                        rootMessage(timelineEvent.root.getDecryptedTextSummary())
-                        lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
+                        rootMessage(timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage)
+                        lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage ?: decryptionErrorMessage)
                         lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
                         lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
                         itemClickListener {

From c04935113008dcfb40ad223e95d268e8d3fdf0c0 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 12 Jan 2022 18:30:43 +0200
Subject: [PATCH 086/130] Fix kltint errors

---
 .../vector/app/features/home/room/detail/TimelineFragment.kt  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 954e62809a..cba99866a9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1917,9 +1917,9 @@ class TimelineFragment @Inject constructor(
                 roomDetailViewModel.handle(action)
             }
             is EncryptedEventContent             -> {
-                if(isRootThreadEvent){
+                if (isRootThreadEvent) {
                     onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
-                }else {
+                } else {
                     roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
                 }
             }

From b89054685f7a95b7fc4dbff27cbe13bb6ef64e50 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 12 Jan 2022 18:40:33 +0200
Subject: [PATCH 087/130] Fix migration from 21 to 22

---
 .../sdk/internal/database/RealmSessionStoreMigration.kt        | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

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 52c7a5cef1..e7ccae38d5 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
@@ -91,6 +91,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion <= 18) migrateTo19(realm)
         if (oldVersion <= 19) migrateTo20(realm)
         if (oldVersion <= 20) migrateTo21(realm)
+        if (oldVersion <= 21) migrateTo22(realm)
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
@@ -447,7 +448,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
                 }
     }
 
-    private fun migrateTo21(realm: DynamicRealm) {
+    private fun migrateTo22(realm: DynamicRealm) {
         Timber.d("Step 21 -> 22")
         val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
 

From 53fecef2d479b54e6e156b9cd485ee889dd3ff75 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 12 Jan 2022 18:47:34 +0200
Subject: [PATCH 088/130] Fix compilation error on TimelineFragment

---
 .../im/vector/app/features/home/room/detail/TimelineFragment.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index cba99866a9..b7af9caf15 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -588,7 +588,7 @@ class TimelineFragment @Inject constructor(
     private fun handleOpenRoomSettings(directAccess: Int? = null) {
         navigator.openRoomProfile(
                 requireContext(),
-                roomDetailArgs.roomId,
+                timelineArgs.roomId,
                 directAccess
         )
     }

From 53b82dfa3f46092970f4c6edd2374f777faf54cc Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 14 Jan 2022 13:02:08 +0200
Subject: [PATCH 089/130] Fix permalink handling for threads regarding timeline
 changes

---
 .../sdk/internal/session/room/timeline/DefaultTimeline.kt   | 5 ++++-
 .../home/room/threads/list/model/ThreadListModel.kt         | 2 +-
 vector/src/main/res/layout/item_thread_list.xml             | 5 +++--
 vector/src/main/res/layout/item_timeline_event_base.xml     | 4 ++--
 .../src/main/res/layout/view_room_detail_thread_toolbar.xml | 2 +-
 vector/src/main/res/layout/view_thread_room_summary.xml     | 6 +++---
 6 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 037edbd83d..b2b033c0bb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -239,7 +239,10 @@ internal class DefaultTimeline(private val roomId: String,
             else                      -> buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
         }
 
-        initPaginationStates(eventId)
+        rootThreadEventId?.let {
+            initPaginationStates(null)
+        } ?: initPaginationStates(eventId)
+
         strategy.onStart()
         loadMore(
                 count = strategyDependencies.timelineSettings.initialSize,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
index b890952719..72ba673972 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
@@ -60,7 +60,7 @@ abstract class ThreadListModel : VectorEpoxyModel() {
         holder.dateTextView.text = date
         if (rootMessageDeleted) {
             holder.rootMessageTextView.text = holder.view.context.getString(R.string.event_redacted)
-            holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.colorOnPrimary)
+            holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.vctr_content_tertiary)
             holder.rootMessageTextView.compoundDrawablePadding = DimensionConverter(holder.view.context.resources).dpToPx(10)
         } else {
             holder.rootMessageTextView.text = rootMessage
diff --git a/vector/src/main/res/layout/item_thread_list.xml b/vector/src/main/res/layout/item_thread_list.xml
index 6a1d075b7c..37186f031c 100644
--- a/vector/src/main/res/layout/item_thread_list.xml
+++ b/vector/src/main/res/layout/item_thread_list.xml
@@ -45,7 +45,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
-        android:layout_marginEnd="25dp"
+        android:layout_marginEnd="28dp"
         android:gravity="end"
         android:maxLines="1"
         android:textColor="?vctr_content_secondary"
@@ -73,6 +73,7 @@
         style="@style/Widget.Vector.TextView.Body"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginTop="3dp"
         android:layout_marginEnd="25dp"
         android:ellipsize="end"
         android:maxLines="2"
@@ -91,7 +92,7 @@
         android:maxWidth="496dp"
         android:minWidth="144dp"
         android:paddingTop="10dp"
-        android:paddingBottom="10dp"
+        android:paddingBottom="12dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="@id/threadSummaryTitleTextView"
         app:layout_constraintTop_toBottomOf="@id/threadSummaryRootMessageTextView"
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 69712a02fc..532cd355c5 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -205,8 +205,8 @@
         android:contentDescription="@string/room_threads_filter"
         android:maxWidth="496dp"
         android:minWidth="144dp"
-        android:paddingTop="10dp"
-        android:paddingBottom="10dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
         android:visibility="gone"
         tools:visibility="visible">
 
diff --git a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
index 5de8ec3efa..af65dee2b4 100644
--- a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
+++ b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
@@ -37,7 +37,7 @@
 
     
+        tools:text="123" />
 
     
Date: Fri, 14 Jan 2022 15:11:20 +0200
Subject: [PATCH 090/130] Add empty screen UI on empty thread list

---
 .../threads/list/views/ThreadListFragment.kt  |  7 ++
 .../main/res/layout/fragment_thread_list.xml  | 87 +++++++++++++++++--
 vector/src/main/res/values/strings.xml        |  3 +
 3 files changed, 91 insertions(+), 6 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
index 2dcd6a48a3..281a292c39 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
@@ -36,6 +36,7 @@ import im.vector.app.features.home.room.threads.ThreadsActivity
 import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
+import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.MatrixItem
 import javax.inject.Inject
@@ -90,6 +91,7 @@ class ThreadListFragment @Inject constructor(
     }
 
     override fun invalidate() = withState(threadListViewModel) { state ->
+        renderEmptyStateIfNeeded(state)
         threadListController.update(state)
     }
 
@@ -104,4 +106,9 @@ class ThreadListFragment @Inject constructor(
     override fun onThreadClicked(timelineEvent: TimelineEvent) {
         (activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent)
     }
+
+    private fun renderEmptyStateIfNeeded(state: ThreadListViewState) {
+        val show = state.rootThreadEventList.invoke().isNullOrEmpty()
+        views.threadListEmptyConstraintLayout.isVisible = show
+    }
 }
diff --git a/vector/src/main/res/layout/fragment_thread_list.xml b/vector/src/main/res/layout/fragment_thread_list.xml
index be042a7bce..77f46bf3ee 100644
--- a/vector/src/main/res/layout/fragment_thread_list.xml
+++ b/vector/src/main/res/layout/fragment_thread_list.xml
@@ -1,6 +1,7 @@
 
 
 
@@ -25,16 +26,90 @@
     
 
     
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/threadListAppBarLayout"
+        tools:listitem="@layout/item_thread_list"
+        tools:visibility="gone" />
+
+    
+
+        
+
+        
+
+        
+
+        
 
 
+    
 
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index d88b7a6f2f..b86b2dd27f 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -1046,6 +1046,9 @@
     Shows all threads from current room
     My Threads
     Shows all threads you’ve participated in
+    Keep discussions organised with threads
+    Threads help keep your conversations on-topic and easy to track.
+    Tip: Long tap a message and use “Reply in thread”.
 
     
     Reason for reporting this content

From 3a3cce85f883d67b37c1442ee2bf4b11b50da4aa Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Fri, 14 Jan 2022 18:42:57 +0200
Subject: [PATCH 091/130] Add encryption shield Change thread list filtering UI
 tick to radio buttons

---
 .../home/room/detail/TimelineFragment.kt      |  3 ++
 .../home/room/detail/search/SearchFragment.kt |  7 ++++-
 .../home/room/threads/ThreadsActivity.kt      |  1 +
 .../room/threads/arguments/ThreadListArgs.kt  |  2 ++
 .../threads/arguments/ThreadTimelineArgs.kt   |  2 ++
 .../list/views/ThreadListBottomSheet.kt       | 31 +++++++++++++++++--
 .../threads/list/views/ThreadListFragment.kt  |  1 +
 .../features/navigation/DefaultNavigator.kt   |  3 +-
 .../features/permalink/PermalinkHandler.kt    |  7 ++++-
 vector/src/main/res/drawable/ic_filter.xml    | 15 ++-------
 .../res/layout/bottom_sheet_thread_list.xml   |  1 -
 .../view_room_detail_thread_toolbar.xml       |  9 ++++++
 12 files changed, 62 insertions(+), 20 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index b7af9caf15..2c4ac2d9d7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1594,6 +1594,7 @@ class TimelineFragment @Inject constructor(
             timelineArgs.threadTimelineArgs?.let {
                 val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl)
                 avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
+                views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel)
                 views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
             }
             views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
@@ -2321,6 +2322,7 @@ class TimelineFragment @Inject constructor(
                     roomId = timelineArgs.roomId,
                     displayName = roomDetailViewModel.getRoomSummary()?.displayName,
                     avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl,
+                    roomEncryptionTrustLevel = roomDetailViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
                     rootThreadEventId = rootThreadEventId)
             navigator.openThread(it, roomThreadDetailArgs)
         }
@@ -2336,6 +2338,7 @@ class TimelineFragment @Inject constructor(
             val roomThreadDetailArgs = ThreadTimelineArgs(
                     roomId = timelineArgs.roomId,
                     displayName = roomDetailViewModel.getRoomSummary()?.displayName,
+                    roomEncryptionTrustLevel = roomDetailViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
                     avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl)
             navigator.openThreadList(it, roomThreadDetailArgs)
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
index aba5c65ae8..62c142238e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
@@ -126,7 +126,12 @@ class SearchFragment @Inject constructor(
     private fun navigateToEvent(event: Event) {
         val roomId = event.roomId ?: return
         event.getRootThreadEventId()?.let {
-            val threadTimelineArgs = ThreadTimelineArgs(roomId, displayName = fragmentArgs.roomDisplayName, fragmentArgs.roomAvatarUrl, it)
+            val threadTimelineArgs = ThreadTimelineArgs(
+                    roomId = roomId,
+                    displayName = fragmentArgs.roomDisplayName,
+                    avatarUrl = fragmentArgs.roomAvatarUrl,
+                    roomEncryptionTrustLevel = null,
+                    rootThreadEventId = it)
             navigator.openThread(requireContext(), threadTimelineArgs, event.eventId)
         } ?: navigator.openRoom(requireContext(), roomId, event.eventId)
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index fe3c32ae65..b9d77a323a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -100,6 +100,7 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon
                 roomId = timelineEvent.roomId,
                 displayName = timelineEvent.senderInfo.displayName,
                 avatarUrl = timelineEvent.senderInfo.avatarUrl,
+                roomEncryptionTrustLevel = null,
                 rootThreadEventId = timelineEvent.eventId)
         val commonOption: (FragmentTransaction) -> Unit = {
             it.setCustomAnimations(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt
index 50819a3017..aa3746ea41 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt
@@ -18,10 +18,12 @@ package im.vector.app.features.home.room.threads.arguments
 
 import android.os.Parcelable
 import kotlinx.parcelize.Parcelize
+import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 
 @Parcelize
 data class ThreadListArgs(
         val roomId: String,
         val displayName: String?,
         val avatarUrl: String?,
+        val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?
 ) : Parcelable
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt
index 2ebed2f745..aadad3d97c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt
@@ -18,11 +18,13 @@ package im.vector.app.features.home.room.threads.arguments
 
 import android.os.Parcelable
 import kotlinx.parcelize.Parcelize
+import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 
 @Parcelize
 data class ThreadTimelineArgs(
         val roomId: String,
         val displayName: String?,
         val avatarUrl: String?,
+        val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?,
         val rootThreadEventId: String? = null
 ) : Parcelable
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
index bd62f65897..7ad4804e5b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListBottomSheet.kt
@@ -16,17 +16,21 @@
 
 package im.vector.app.features.home.room.threads.list.views
 
+import android.graphics.drawable.Drawable
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import androidx.annotation.AttrRes
 import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
 import com.airbnb.mvrx.parentFragmentViewModel
 import im.vector.app.R
 import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
 import im.vector.app.databinding.BottomSheetThreadListBinding
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
 import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
+import im.vector.app.features.themes.ThemeUtils
 
 class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment() {
 
@@ -53,8 +57,29 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment
   
-  
+      android:pathData="M10.9996,18H12.9996C13.5496,18 13.9996,17.55 13.9996,17C13.9996,16.45 13.5496,16 12.9996,16H10.9996C10.4496,16 9.9996,16.45 9.9996,17C9.9996,17.55 10.4496,18 10.9996,18ZM2.9996,7C2.9996,7.55 3.4496,8 3.9996,8H19.9996C20.5496,8 20.9996,7.55 20.9996,7C20.9996,6.45 20.5496,6 19.9996,6H3.9996C3.4496,6 2.9996,6.45 2.9996,7ZM6.9996,13H16.9996C17.5496,13 17.9996,12.55 17.9996,12C17.9996,11.45 17.5496,11 16.9996,11H6.9996C6.4496,11 5.9996,11.45 5.9996,12C5.9996,12.55 6.4496,13 6.9996,13Z"
+      android:fillColor="#737D8C"/>
 
diff --git a/vector/src/main/res/layout/bottom_sheet_thread_list.xml b/vector/src/main/res/layout/bottom_sheet_thread_list.xml
index 3fd75e1823..e736f30edc 100644
--- a/vector/src/main/res/layout/bottom_sheet_thread_list.xml
+++ b/vector/src/main/res/layout/bottom_sheet_thread_list.xml
@@ -1,7 +1,6 @@
 
 
 
+    
+
     
Date: Mon, 17 Jan 2022 14:26:39 +0200
Subject: [PATCH 092/130] Remove duplicate RetryTestRule

---
 .../sdk/session/room/threads/RetryTestRule.kt | 58 -------------------
 1 file changed, 58 deletions(-)
 delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
deleted file mode 100644
index c06a18aeb3..0000000000
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 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.session.room.threads
-
-import android.util.Log
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Retry test rule used to retry test that failed.
- * Retry failed test 3 times
- */
-class RetryTestRule(val retryCount: Int = 3) : TestRule {
-
-    private val TAG = RetryTestRule::class.java.simpleName
-
-    override fun apply(base: Statement, description: Description): Statement {
-        return statement(base, description)
-    }
-
-    private fun statement(base: Statement, description: Description): Statement {
-        return object : Statement() {
-            @Throws(Throwable::class)
-            override fun evaluate() {
-                var caughtThrowable: Throwable? = null
-
-                // implement retry logic here
-                for (i in 0 until retryCount) {
-                    try {
-                        base.evaluate()
-                        return
-                    } catch (t: Throwable) {
-                        caughtThrowable = t
-                        Log.e(TAG, description.displayName + ": run " + (i + 1) + " failed")
-                    }
-                }
-
-                Log.e(TAG, description.displayName + ": giving up after " + retryCount + " failures")
-                throw caughtThrowable!!
-            }
-        }
-    }
-}

From b343739a71c7968ffe1d0e2087975975a954b981 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 17 Jan 2022 14:27:17 +0200
Subject: [PATCH 093/130] Enhance decrypted thread summary to return poll
 questions

---
 .../android/sdk/api/session/events/model/Event.kt   | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

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 895280732e..147eff6ab0 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
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 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.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
@@ -205,7 +206,7 @@ data class Event(
             isAudioMessage()       -> "sent an audio file."
             isImageMessage()       -> "sent an image."
             isVideoMessage()       -> "sent a video."
-            isPoll()               -> "created a poll."
+            isPoll()               -> getPollQuestion() ?: "created a poll."
             else                   -> text
         }
     }
@@ -357,6 +358,12 @@ fun Event.getRelationContent(): RelationDefaultContent? {
     }
 }
 
+/**
+ * Returns the poll question or null otherwise
+ */
+fun Event.getPollQuestion(): String? =
+        getPollContent()?.pollCreationInfo?.question?.question
+
 /**
  * Returns the relation content for a specific type or null otherwise
  */
@@ -381,3 +388,7 @@ fun Event.getPresenceContent(): PresenceContent? {
 
 fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
         content?.toModel()?.membership == Membership.INVITE
+
+fun Event.getPollContent(): MessagePollContent? {
+    return content.toModel()
+}

From f6067977fea62e011edde4856743ad719bb1610d Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 17 Jan 2022 14:27:30 +0200
Subject: [PATCH 094/130] Refactor ThreadMessagingTest

---
 .../session/room/threads/ThreadMessagingTest.kt   | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
index 62887beba7..6aa4f4cc32 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/ThreadMessagingTest.kt
@@ -40,15 +40,10 @@ import java.util.concurrent.CountDownLatch
 @FixMethodOrder(MethodSorters.JVM)
 class ThreadMessagingTest : InstrumentedTest {
 
-    private val commonTestHelper = CommonTestHelper(context())
-    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
-//    @Rule
-//    @JvmField
-//    val mRetryTestRule = RetryTestRule()
-
     @Test
     fun reply_in_thread_should_create_a_thread() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
 
         val aliceSession = cryptoTestData.firstSession
@@ -105,6 +100,8 @@ class ThreadMessagingTest : InstrumentedTest {
 
     @Test
     fun reply_in_thread_should_create_a_thread_from_other_user() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
 
         val aliceSession = cryptoTestData.firstSession
@@ -176,6 +173,8 @@ class ThreadMessagingTest : InstrumentedTest {
 
     @Test
     fun reply_in_thread_to_timeline_message_multiple_times() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
 
         val aliceSession = cryptoTestData.firstSession
@@ -237,6 +236,8 @@ class ThreadMessagingTest : InstrumentedTest {
 
     @Test
     fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
 
         val aliceSession = cryptoTestData.firstSession

From 81a1dfd66d4da487b2880ea0de8895095d2747bd Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 17 Jan 2022 17:28:40 +0200
Subject: [PATCH 095/130] PR Remarks

---
 .../main/res/anim/animation_slide_in_left.xml |   0
 .../res/anim/animation_slide_in_right.xml     |   0
 .../res/anim/animation_slide_out_left.xml     |   0
 .../res/anim/animation_slide_out_right.xml    |   0
 .../sdk/api/session/events/model/Event.kt     |   4 +-
 .../api/session/events/model/RelationType.kt  |   2 +-
 .../session/room/timeline/TimelineService.kt  |   4 +-
 .../threads/ThreadNotificationState.kt        |   2 +-
 .../session/room/timeline/LoadMoreResult.kt   |   1 -
 .../handler/room/ThreadsAwarenessHandler.kt   |   2 +-
 .../im/vector/app/core/di/FragmentModule.kt   |   4 +-
 .../app/core/di/MavericksViewModelModule.kt   |   6 +-
 .../im/vector/app/core/extensions/TextView.kt |   2 +-
 .../detail/JoinReplacementRoomBottomSheet.kt  |   2 +-
 .../room/detail/StartCallActionsHandler.kt    |  16 +-
 .../home/room/detail/TimelineFragment.kt      | 180 +++++++++---------
 ...etailViewModel.kt => TimelineViewModel.kt} |   8 +-
 .../composer/MessageComposerViewModel.kt      |  14 +-
 .../timeline/action/EventSharedAction.kt      |   2 -
 .../action/MessageActionsViewModel.kt         |   7 +-
 .../detail/views/RoomDetailLazyLoadedViews.kt |   6 +-
 .../detail/widget/RoomWidgetsBottomSheet.kt   |  10 +-
 .../{ThreadListModel.kt => ThreadListItem.kt} |   6 +-
 .../list/viewmodel/ThreadListController.kt    |   4 +-
 .../threads/list/views/ThreadListFragment.kt  |   7 +
 .../main/res/layout/fragment_thread_list.xml  |   4 +-
 ..._room_detail.xml => fragment_timeline.xml} |   0
 .../{item_thread_list.xml => item_thread.xml} |   0
 .../view_room_detail_thread_toolbar.xml       |   2 +-
 .../res/layout/view_thread_room_summary.xml   |   2 +-
 vector/src/main/res/menu/menu_timeline.xml    |   2 +-
 vector/src/main/res/values/strings.xml        |   4 +-
 32 files changed, 152 insertions(+), 151 deletions(-)
 rename {vector => library/ui-styles}/src/main/res/anim/animation_slide_in_left.xml (100%)
 rename {vector => library/ui-styles}/src/main/res/anim/animation_slide_in_right.xml (100%)
 rename {vector => library/ui-styles}/src/main/res/anim/animation_slide_out_left.xml (100%)
 rename {vector => library/ui-styles}/src/main/res/anim/animation_slide_out_right.xml (100%)
 rename vector/src/main/java/im/vector/app/features/home/room/detail/{RoomDetailViewModel.kt => TimelineViewModel.kt} (99%)
 rename vector/src/main/java/im/vector/app/features/home/room/threads/list/model/{ThreadListModel.kt => ThreadListItem.kt} (96%)
 rename vector/src/main/res/layout/{fragment_room_detail.xml => fragment_timeline.xml} (100%)
 rename vector/src/main/res/layout/{item_thread_list.xml => item_thread.xml} (100%)

diff --git a/vector/src/main/res/anim/animation_slide_in_left.xml b/library/ui-styles/src/main/res/anim/animation_slide_in_left.xml
similarity index 100%
rename from vector/src/main/res/anim/animation_slide_in_left.xml
rename to library/ui-styles/src/main/res/anim/animation_slide_in_left.xml
diff --git a/vector/src/main/res/anim/animation_slide_in_right.xml b/library/ui-styles/src/main/res/anim/animation_slide_in_right.xml
similarity index 100%
rename from vector/src/main/res/anim/animation_slide_in_right.xml
rename to library/ui-styles/src/main/res/anim/animation_slide_in_right.xml
diff --git a/vector/src/main/res/anim/animation_slide_out_left.xml b/library/ui-styles/src/main/res/anim/animation_slide_out_left.xml
similarity index 100%
rename from vector/src/main/res/anim/animation_slide_out_left.xml
rename to library/ui-styles/src/main/res/anim/animation_slide_out_left.xml
diff --git a/vector/src/main/res/anim/animation_slide_out_right.xml b/library/ui-styles/src/main/res/anim/animation_slide_out_right.xml
similarity index 100%
rename from vector/src/main/res/anim/animation_slide_out_right.xml
rename to library/ui-styles/src/main/res/anim/animation_slide_out_right.xml
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 147eff6ab0..047aefe88d 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
@@ -222,8 +222,8 @@ data class Event(
     private fun getDecryptedValue(key: String = "body"): String? {
         return if (isEncrypted()) {
             @Suppress("UNCHECKED_CAST")
-            val content = mxDecryptionResult?.payload?.get("content") as? JsonDict
-            content?.get(key) as? String
+            val decryptedContent = mxDecryptionResult?.payload?.get("content") as? JsonDict
+            decryptedContent?.get(key) as? String
         } else {
             content?.get(key) as? String
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
index 18bb946462..fb26264ad7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
@@ -28,7 +28,7 @@ object RelationType {
     /** Lets you define an event which references an existing event.*/
     const val REFERENCE = "m.reference"
 
-    /** Lets you define an event which is a reply to an existing event.*/
+    /** Lets you define an event which is a thread reply to an existing event.*/
     const val THREAD = "m.thread"
     const val IO_THREAD = "io.element.thread"
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index bf48353918..aefda755f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -57,13 +57,13 @@ interface TimelineService {
     fun getAttachmentMessages(): List
 
     /**
-     * Get a live list of all the thread for the specified roomId
+     * Get a live list of all the TimelineEvents which have thread replies for the specified roomId
      * @return the [LiveData] of [TimelineEvent]
      */
     fun getAllThreadsLive(): LiveData>
 
     /**
-     * Get a list of all the thread for the specified roomId
+     * Get a list of all the TimelineEvents which have thread replies for the specified roomId
      * @return the [LiveData] of [TimelineEvent]
      */
     fun getAllThreads(): List
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
index 58cc3a0706..8566d68aa5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadNotificationState.kt
@@ -27,7 +27,7 @@ enum class ThreadNotificationState {
     // There is at least one new message
     NEW_MESSAGE,
 
-    // The is at least one new message that should bi highlighted
+    // The is at least one new message that should be highlighted
     // ex. "Hello @aris.kotsomitopoulos"
     NEW_HIGHLIGHTED_MESSAGE;
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
index 2949e35bd3..c419e8325e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
@@ -20,5 +20,4 @@ internal enum class LoadMoreResult {
     REACHED_END,
     SUCCESS,
     FAILURE
-    // evenIDS
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index 24854b601f..ee606d1fac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -223,7 +223,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 body)
 
         val messageTextContent = MessageTextContent(
-                msgType = "m.text",
+                msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = body,
                 formattedBody = replyFormatted
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 83ebefb658..b954634129 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -203,7 +203,7 @@ interface FragmentModule {
     @Binds
     @IntoMap
     @FragmentKey(TimelineFragment::class)
-    fun bindRoomDetailFragment(fragment: TimelineFragment): Fragment
+    fun bindTimelineFragment(fragment: TimelineFragment): Fragment
 
     @Binds
     @IntoMap
@@ -933,7 +933,7 @@ interface FragmentModule {
     @Binds
     @IntoMap
     @FragmentKey(ThreadListFragment::class)
-    fun bindRoomThreadDetailFragment(fragment: ThreadListFragment): Fragment
+    fun bindThreadListFragment(fragment: ThreadListFragment): Fragment
 
     @Binds
     @IntoMap
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index cc31a7dca6..98b1a5d048 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -44,7 +44,7 @@ import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
 import im.vector.app.features.home.UnreadMessagesSharedViewModel
 import im.vector.app.features.home.UserColorAccountDataViewModel
 import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
-import im.vector.app.features.home.room.detail.RoomDetailViewModel
+import im.vector.app.features.home.room.detail.TimelineViewModel
 import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
 import im.vector.app.features.home.room.detail.search.SearchViewModel
 import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
@@ -536,8 +536,8 @@ interface MavericksViewModelModule {
 
     @Binds
     @IntoMap
-    @MavericksViewModelKey(RoomDetailViewModel::class)
-    fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+    @MavericksViewModelKey(TimelineViewModel::class)
+    fun roomDetailViewModelFactory(factory: TimelineViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
 
     @Binds
     @IntoMap
diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
index c0911aec8b..8e7b6a4a80 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt
@@ -126,7 +126,7 @@ fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @AttrRes tintColor: Int?
 }
 
 fun TextView.clearDrawables() {
-    this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
+    setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
 }
 
 /**
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt
index ba559677c9..99843084ec 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JoinReplacementRoomBottomSheet.kt
@@ -44,7 +44,7 @@ class JoinReplacementRoomBottomSheet :
     @Inject
     lateinit var errorFormatter: ErrorFormatter
 
-    private val viewModel: RoomDetailViewModel by parentFragmentViewModel()
+    private val viewModel: TimelineViewModel by parentFragmentViewModel()
 
     override val showExpanded: Boolean
         get() = true
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
index 6b5ed3ba66..193dc42f33 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
@@ -32,7 +32,7 @@ class StartCallActionsHandler(
         private val fragment: Fragment,
         private val callManager: WebRtcCallManager,
         private val vectorPreferences: VectorPreferences,
-        private val roomDetailViewModel: RoomDetailViewModel,
+        private val timelineViewModel: TimelineViewModel,
         private val startCallActivityResultLauncher: ActivityResultLauncher>,
         private val showDialogWithMessage: (String) -> Unit,
         private val onTapToReturnToCall: () -> Unit) {
@@ -45,7 +45,7 @@ class StartCallActionsHandler(
         handleCallRequest(false)
     }
 
-    private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state ->
+    private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state ->
         val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
         when (roomSummary.joinedMembersCount) {
             1    -> {
@@ -95,7 +95,7 @@ class StartCallActionsHandler(
                                 .setMessage(R.string.audio_video_meeting_description)
                                 .setPositiveButton(fragment.getString(R.string.create)) { _, _ ->
                                     // create the widget, then navigate to it..
-                                    roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
+                                    timelineViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
                                 }
                                 .setNegativeButton(fragment.getString(R.string.action_cancel), null)
                                 .show()
@@ -121,22 +121,22 @@ class StartCallActionsHandler(
 
     private fun safeStartCall2(isVideoCall: Boolean) {
         val startCallAction = RoomDetailAction.StartCall(isVideoCall)
-        roomDetailViewModel.pendingAction = startCallAction
+        timelineViewModel.pendingAction = startCallAction
         if (isVideoCall) {
             if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL,
                             fragment.requireActivity(),
                             startCallActivityResultLauncher,
                             R.string.permissions_rationale_msg_camera_and_audio)) {
-                roomDetailViewModel.pendingAction = null
-                roomDetailViewModel.handle(startCallAction)
+                timelineViewModel.pendingAction = null
+                timelineViewModel.handle(startCallAction)
             }
         } else {
             if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL,
                             fragment.requireActivity(),
                             startCallActivityResultLauncher,
                             R.string.permissions_rationale_msg_record_audio)) {
-                roomDetailViewModel.pendingAction = null
-                roomDetailViewModel.handle(startCallAction)
+                timelineViewModel.pendingAction = null
+                timelineViewModel.handle(startCallAction)
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 2c4ac2d9d7..1401a045fc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -117,7 +117,7 @@ import im.vector.app.core.utils.shareText
 import im.vector.app.core.utils.startInstallFromSourceIntent
 import im.vector.app.core.utils.toast
 import im.vector.app.databinding.DialogReportContentBinding
-import im.vector.app.databinding.FragmentRoomDetailBinding
+import im.vector.app.databinding.FragmentTimelineBinding
 import im.vector.app.features.attachments.AttachmentTypeSelectorView
 import im.vector.app.features.attachments.AttachmentsHelper
 import im.vector.app.features.attachments.ContactAttachment
@@ -255,7 +255,7 @@ class TimelineFragment @Inject constructor(
         private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
         private val clock: Clock
 ) :
-        VectorBaseFragment(),
+        VectorBaseFragment(),
         TimelineEventController.Callback,
         VectorInviteView.Callback,
         AttachmentTypeSelectorView.Callback,
@@ -295,15 +295,15 @@ class TimelineFragment @Inject constructor(
         autoCompleterFactory.create(timelineArgs.roomId, isThreadTimeLine())
     }
 
-    private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
+    private val timelineViewModel: TimelineViewModel by fragmentViewModel()
     private val messageComposerViewModel: MessageComposerViewModel by fragmentViewModel()
     private val debouncer = Debouncer(createUIHandler())
 
     private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
     private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
 
-    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomDetailBinding {
-        return FragmentRoomDetailBinding.inflate(inflater, container, false)
+    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTimelineBinding {
+        return FragmentTimelineBinding.inflate(inflater, container, false)
     }
 
     override fun getMenuRes() = R.menu.menu_timeline
@@ -335,7 +335,7 @@ class TimelineFragment @Inject constructor(
         super.onCreate(savedInstanceState)
         setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
             bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
-                roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
+                timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
             }
         }
     }
@@ -351,7 +351,7 @@ class TimelineFragment @Inject constructor(
                 roomId = timelineArgs.roomId,
                 fragment = this,
                 vectorPreferences = vectorPreferences,
-                roomDetailViewModel = roomDetailViewModel,
+                timelineViewModel = timelineViewModel,
                 callManager = callManager,
                 startCallActivityResultLauncher = startCallActivityResultLauncher,
                 showDialogWithMessage = ::showDialogWithMessage,
@@ -388,7 +388,7 @@ class TimelineFragment @Inject constructor(
                     invalidateOptionsMenu()
                 }
 
-        roomDetailViewModel.onEach(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
+        timelineViewModel.onEach(RoomDetailViewState::canShowJumpToReadMarker, RoomDetailViewState::unreadState) { _, _ ->
             updateJumpToReadMarkerViewVisibility()
         }
 
@@ -405,7 +405,7 @@ class TimelineFragment @Inject constructor(
             }
         }
 
-        roomDetailViewModel.onEach(
+        timelineViewModel.onEach(
                 RoomDetailViewState::syncState,
                 RoomDetailViewState::incrementalSyncStatus,
                 RoomDetailViewState::pushCounter
@@ -435,7 +435,7 @@ class TimelineFragment @Inject constructor(
             }.exhaustive
         }
 
-        roomDetailViewModel.observeViewEvents {
+        timelineViewModel.observeViewEvents {
             when (it) {
                 is RoomDetailViewEvents.Failure                          -> showErrorInSnackbar(it.throwable)
                 is RoomDetailViewEvents.OnNewTimelineEvents              -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
@@ -501,12 +501,12 @@ class TimelineFragment @Inject constructor(
 
     private fun setupRemoveJitsiWidgetView() {
         views.removeJitsiWidgetView.onCompleteSliding = {
-            withState(roomDetailViewModel) {
+            withState(timelineViewModel) {
                 val jitsiWidgetId = it.jitsiState.widgetId ?: return@withState
                 if (it.jitsiState.hasJoined) {
                     leaveJitsiConference()
                 }
-                roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidgetId))
+                timelineViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidgetId))
             }
         }
     }
@@ -516,7 +516,7 @@ class TimelineFragment @Inject constructor(
     }
 
     private fun onBroadcastJitsiEvent(conferenceEvent: ConferenceEvent) {
-        roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(conferenceEvent))
+        timelineViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(conferenceEvent))
     }
 
     private fun onCannotRecord() {
@@ -577,7 +577,7 @@ class TimelineFragment @Inject constructor(
 
     override fun onImageReady(uri: Uri?) {
         uri ?: return
-        roomDetailViewModel.handle(
+        timelineViewModel.handle(
                 RoomDetailAction.SetAvatarAction(
                         newAvatarUri = uri,
                         newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()
@@ -616,7 +616,7 @@ class TimelineFragment @Inject constructor(
             ).apply {
                 directListener = { granted ->
                     if (granted) {
-                        roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
+                        timelineViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
                                 widget = it.widget,
                                 userJustAccepted = true,
                                 grantedEvents = it.grantedEvents
@@ -696,13 +696,13 @@ class TimelineFragment @Inject constructor(
                         .setMessage(getString(R.string.event_status_delete_all_failed_dialog_message))
                         .setNegativeButton(R.string.no, null)
                         .setPositiveButton(R.string.yes) { _, _ ->
-                            roomDetailViewModel.handle(RoomDetailAction.RemoveAllFailedMessages)
+                            timelineViewModel.handle(RoomDetailAction.RemoveAllFailedMessages)
                         }
                         .show()
             }
 
             override fun onRetryClicked() {
-                roomDetailViewModel.handle(RoomDetailAction.ResendAll)
+                timelineViewModel.handle(RoomDetailAction.ResendAll)
             }
         }
     }
@@ -799,7 +799,7 @@ class TimelineFragment @Inject constructor(
         val safeContext = context ?: return
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             if (!safeContext.packageManager.canRequestPackageInstalls()) {
-                roomDetailViewModel.pendingEvent = action
+                timelineViewModel.pendingEvent = action
                 startInstallFromSourceIntent(safeContext, installApkActivityResultLauncher)
             } else {
                 openFile(action)
@@ -811,7 +811,7 @@ class TimelineFragment @Inject constructor(
 
     private val installApkActivityResultLauncher = registerStartForActivityResult { activityResult ->
         if (activityResult.resultCode == Activity.RESULT_OK) {
-            roomDetailViewModel.pendingEvent?.let {
+            timelineViewModel.pendingEvent?.let {
                 if (it is RoomDetailViewEvents.OpenFile) {
                     openFile(it)
                 }
@@ -819,7 +819,7 @@ class TimelineFragment @Inject constructor(
         } else {
             // User cancelled
         }
-        roomDetailViewModel.pendingEvent = null
+        timelineViewModel.pendingEvent = null
     }
 
     private fun displayPromptForIntegrationManager() {
@@ -879,18 +879,18 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onDestroy() {
-        roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
+        timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
         super.onDestroy()
     }
 
     private fun setupJumpToBottomView() {
         views.jumpToBottomView.visibility = View.INVISIBLE
         views.jumpToBottomView.debouncedClicks {
-            roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
+            timelineViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
             views.jumpToBottomView.visibility = View.INVISIBLE
-            if (!roomDetailViewModel.timeline.isLive) {
+            if (!timelineViewModel.timeline.isLive) {
                 scrollOnNewMessageCallback.forceScrollOnNextUpdate()
-                roomDetailViewModel.timeline.restartWithEventId(null)
+                timelineViewModel.timeline.restartWithEventId(null)
             } else {
                 layoutManager.scrollToPosition(0)
             }
@@ -909,7 +909,7 @@ class TimelineFragment @Inject constructor(
             onJumpToReadMarkerClicked()
         }
         views.jumpToReadMarkerView.setOnCloseIconClickListener {
-            roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
+            timelineViewModel.handle(RoomDetailAction.MarkAllAsRead)
         }
     }
 
@@ -952,11 +952,11 @@ class TimelineFragment @Inject constructor(
     private fun setupNotificationView() {
         views.notificationAreaView.delegate = object : NotificationAreaView.Delegate {
             override fun onTombstoneEventClicked() {
-                roomDetailViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom)
+                timelineViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom)
             }
 
             override fun onMisconfiguredEncryptionClicked() {
-                roomDetailViewModel.handle(RoomDetailAction.OnClickMisconfiguredEncryption)
+                timelineViewModel.handle(RoomDetailAction.OnClickMisconfiguredEncryption)
             }
         }
     }
@@ -971,7 +971,7 @@ class TimelineFragment @Inject constructor(
         }
         val joinConfItem = menu.findItem(R.id.join_conference)
         (joinConfItem.actionView as? JoinConferenceView)?.onJoinClicked = {
-            roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall)
+            timelineViewModel.handle(RoomDetailAction.JoinJitsiCall)
         }
 
         // Custom thread notification menu item
@@ -984,10 +984,10 @@ class TimelineFragment @Inject constructor(
 
     override fun onPrepareOptionsMenu(menu: Menu) {
         menu.forEach {
-            it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
+            it.isVisible = timelineViewModel.isMenuItemVisible(it.itemId)
         }
 
-        withState(roomDetailViewModel) { state ->
+        withState(timelineViewModel) { state ->
             // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions
             val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined
             val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
@@ -1035,7 +1035,7 @@ class TimelineFragment @Inject constructor(
                 true
             }
             R.id.open_matrix_apps                  -> {
-                roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
+                timelineViewModel.handle(RoomDetailAction.ManageIntegrations)
                 true
             }
             R.id.voice_call                        -> {
@@ -1123,8 +1123,8 @@ class TimelineFragment @Inject constructor(
             navigator.openSearch(
                     context = requireContext(),
                     roomId = timelineArgs.roomId,
-                    roomDisplayName = roomDetailViewModel.getRoomSummary()?.displayName,
-                    roomAvatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl
+                    roomDisplayName = timelineViewModel.getRoomSummary()?.displayName,
+                    roomAvatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl
             )
         } else {
             showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room))
@@ -1219,11 +1219,11 @@ class TimelineFragment @Inject constructor(
     private fun handlePendingAction(roomDetailPendingAction: RoomDetailPendingAction) {
         when (roomDetailPendingAction) {
             is RoomDetailPendingAction.JumpToReadReceipt ->
-                roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId))
+                timelineViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId))
             is RoomDetailPendingAction.MentionUser       ->
                 insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
             is RoomDetailPendingAction.OpenOrCreateDm    ->
-                roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId))
+                timelineViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId))
             is RoomDetailPendingAction.OpenRoom          ->
                 handleOpenRoom(RoomDetailViewEvents.OpenRoom(roomDetailPendingAction.roomId, roomDetailPendingAction.closeCurrentRoom))
         }.exhaustive
@@ -1276,7 +1276,7 @@ class TimelineFragment @Inject constructor(
         if (activityResult.resultCode == Activity.RESULT_OK) {
             val sendData = AttachmentsPreviewActivity.getOutput(data)
             val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
-            roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
+            timelineViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
         }
     }
 
@@ -1285,7 +1285,7 @@ class TimelineFragment @Inject constructor(
             val eventId = EmojiReactionPickerActivity.getOutputEventId(activityResult.data)
             val reaction = EmojiReactionPickerActivity.getOutputReaction(activityResult.data)
             if (eventId != null && reaction != null) {
-                roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
+                timelineViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
             }
         }
     }
@@ -1295,16 +1295,16 @@ class TimelineFragment @Inject constructor(
         if (activityResult.resultCode == Activity.RESULT_OK) {
             WidgetActivity.getOutput(data).toModel()
                     ?.let { content ->
-                        roomDetailViewModel.handle(RoomDetailAction.SendSticker(content))
+                        timelineViewModel.handle(RoomDetailAction.SendSticker(content))
                     }
         }
     }
 
     private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
         if (allGranted) {
-            (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
-                roomDetailViewModel.pendingAction = null
-                roomDetailViewModel.handle(it)
+            (timelineViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
+                timelineViewModel.pendingAction = null
+                timelineViewModel.handle(it)
             }
         } else {
             if (deniedPermanently) {
@@ -1318,7 +1318,7 @@ class TimelineFragment @Inject constructor(
 
     private fun setupRecyclerView() {
         timelineEventController.callback = this
-        timelineEventController.timeline = roomDetailViewModel.timeline
+        timelineEventController.timeline = timelineViewModel.timeline
 
         views.timelineRecyclerView.trackItemsVisibilityChange()
         layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, true) {
@@ -1388,7 +1388,7 @@ class TimelineFragment @Inject constructor(
     private fun updateJumpToReadMarkerViewVisibility() {
         if (isThreadTimeLine()) return
         viewLifecycleOwner.lifecycleScope.launchWhenResumed {
-            val state = roomDetailViewModel.awaitState()
+            val state = timelineViewModel.awaitState()
             val showJumpToUnreadBanner = when (state.unreadState) {
                 UnreadState.Unknown,
                 UnreadState.HasNoUnread            -> false
@@ -1500,7 +1500,7 @@ class TimelineFragment @Inject constructor(
 
         views.composerLayout.views.composerEditText.focusChanges()
                 .onEach {
-                    roomDetailViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
+                    timelineViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
                 }
                 .launchIn(viewLifecycleOwner.lifecycleScope)
     }
@@ -1514,7 +1514,7 @@ class TimelineFragment @Inject constructor(
         return isHandled
     }
 
-    override fun invalidate() = withState(roomDetailViewModel, messageComposerViewModel) { mainState, messageComposerState ->
+    override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
         invalidateOptionsMenu()
         val summary = mainState.asyncRoomSummary()
         renderToolbar(summary, mainState.formattedTypingUsers)
@@ -1568,7 +1568,7 @@ class TimelineFragment @Inject constructor(
         }
     }
 
-    private fun FragmentRoomDetailBinding.hideComposerViews() {
+    private fun FragmentTimelineBinding.hideComposerViews() {
         composerLayout.isVisible = false
         voiceMessageRecorderView.isVisible = false
     }
@@ -1679,7 +1679,7 @@ class TimelineFragment @Inject constructor(
                 .setView(layout)
                 .setPositiveButton(R.string.report_content_custom_submit) { _, _ ->
                     val reason = views.dialogReportContentInput.text.toString()
-                    roomDetailViewModel.handle(RoomDetailAction.ReportContent(action.eventId, action.senderId, reason))
+                    timelineViewModel.handle(RoomDetailAction.ReportContent(action.eventId, action.senderId, reason))
                 }
                 .setNegativeButton(R.string.action_cancel, null)
                 .show()
@@ -1695,7 +1695,7 @@ class TimelineFragment @Inject constructor(
                         reasonHintRes = R.string.delete_event_dialog_reason_hint,
                         titleRes = action.dialogTitleRes
                 ) { reason ->
-                    roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
+                    timelineViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
                 }
     }
 
@@ -1717,7 +1717,7 @@ class TimelineFragment @Inject constructor(
                                 .setMessage(R.string.content_reported_as_spam_content)
                                 .setPositiveButton(R.string.ok, null)
                                 .setNegativeButton(R.string.block_user) { _, _ ->
-                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
+                                    timelineViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
                                 }
                                 .show()
                     }
@@ -1727,7 +1727,7 @@ class TimelineFragment @Inject constructor(
                                 .setMessage(R.string.content_reported_as_inappropriate_content)
                                 .setPositiveButton(R.string.ok, null)
                                 .setNegativeButton(R.string.block_user) { _, _ ->
-                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
+                                    timelineViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
                                 }
                                 .show()
                     }
@@ -1737,7 +1737,7 @@ class TimelineFragment @Inject constructor(
                                 .setMessage(R.string.content_reported_content)
                                 .setPositiveButton(R.string.ok, null)
                                 .setNegativeButton(R.string.block_user) { _, _ ->
-                                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
+                                    timelineViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
                                 }
                                 .show()
                     }
@@ -1791,7 +1791,7 @@ class TimelineFragment @Inject constructor(
                                     true
                                 } else {
                                     // Highlight and scroll to this event
-                                    roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true))
+                                    timelineViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true))
                                     true
                                 }
                             } else {
@@ -1800,7 +1800,7 @@ class TimelineFragment @Inject constructor(
                                     true
                                 } else if (rootThreadEventId == getRootThreadEventId() && eventId != null) {
                                     // we are in the same thread
-                                    roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true))
+                                    timelineViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true))
                                     true
                                 } else {
                                     false
@@ -1847,11 +1847,11 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onEventVisible(event: TimelineEvent) {
-        roomDetailViewModel.handle(RoomDetailAction.TimelineEventTurnsVisible(event))
+        timelineViewModel.handle(RoomDetailAction.TimelineEventTurnsVisible(event))
     }
 
     override fun onEventInvisible(event: TimelineEvent) {
-        roomDetailViewModel.handle(RoomDetailAction.TimelineEventTurnsInvisible(event))
+        timelineViewModel.handle(RoomDetailAction.TimelineEventTurnsInvisible(event))
     }
 
     override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) {
@@ -1896,7 +1896,7 @@ class TimelineFragment @Inject constructor(
 
     private fun cleanUpAfterPermissionNotGranted() {
         // Reset all pending data
-        roomDetailViewModel.pendingAction = null
+        timelineViewModel.pendingAction = null
         attachmentsHelper.pendingType = null
     }
 
@@ -1905,23 +1905,23 @@ class TimelineFragment @Inject constructor(
 //    }
 
     override fun onLoadMore(direction: Timeline.Direction) {
-        roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
+        timelineViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
     }
 
     override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) {
         when (messageContent) {
             is MessageVerificationRequestContent -> {
-                roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
+                timelineViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
             }
             is MessageWithAttachmentContent      -> {
                 val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, informationData.senderId, messageContent)
-                roomDetailViewModel.handle(action)
+                timelineViewModel.handle(action)
             }
             is EncryptedEventContent             -> {
                 if (isRootThreadEvent) {
                     onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
                 } else {
-                    roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
+                    timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
                 }
             }
             else                                 -> {
@@ -1944,14 +1944,14 @@ class TimelineFragment @Inject constructor(
 
     private fun handleCancelSend(action: EventSharedAction.Cancel) {
         if (action.force) {
-            roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true))
+            timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true))
         } else {
             MaterialAlertDialogBuilder(requireContext())
                     .setTitle(R.string.dialog_title_confirmation)
                     .setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
                     .setNegativeButton(R.string.no, null)
                     .setPositiveButton(R.string.yes) { _, _ ->
-                        roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false))
+                        timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false))
                     }
                     .show()
         }
@@ -1979,10 +1979,10 @@ class TimelineFragment @Inject constructor(
     override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
         if (on) {
             // we should test the current real state of reaction on this event
-            roomDetailViewModel.handle(RoomDetailAction.SendReaction(informationData.eventId, reaction))
+            timelineViewModel.handle(RoomDetailAction.SendReaction(informationData.eventId, reaction))
         } else {
             // I need to redact a reaction
-            roomDetailViewModel.handle(RoomDetailAction.UndoReaction(informationData.eventId, reaction))
+            timelineViewModel.handle(RoomDetailAction.UndoReaction(informationData.eventId, reaction))
         }
     }
 
@@ -1997,11 +1997,11 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onTimelineItemAction(itemAction: RoomDetailAction) {
-        roomDetailViewModel.handle(itemAction)
+        timelineViewModel.handle(itemAction)
     }
 
     override fun getPreviewUrlRetriever(): PreviewUrlRetriever {
-        return roomDetailViewModel.previewUrlRetriever
+        return timelineViewModel.previewUrlRetriever
     }
 
     override fun onRoomCreateLinkClicked(url: String) {
@@ -2022,7 +2022,7 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onReadMarkerVisible() {
-        roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
+        timelineViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
     }
 
     override fun onPreviewUrlClicked(url: String) {
@@ -2030,7 +2030,7 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onPreviewUrlCloseClicked(eventId: String, url: String) {
-        roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
+        timelineViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
     }
 
     override fun onPreviewUrlImageClicked(sharedView: View?, mxcUrl: String?, title: String?) {
@@ -2140,7 +2140,7 @@ class TimelineFragment @Inject constructor(
             }
             is EventSharedAction.QuickReact                 -> {
                 // eventId,ClickedOn,Add
-                roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
+                timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
             }
             is EventSharedAction.Edit                       -> {
                 if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
@@ -2179,20 +2179,20 @@ class TimelineFragment @Inject constructor(
                 showSnackWithMessage(getString(R.string.copied_to_clipboard))
             }
             is EventSharedAction.Resend                     -> {
-                roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
+                timelineViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
             }
             is EventSharedAction.Remove                     -> {
-                roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
+                timelineViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
             }
             is EventSharedAction.Cancel                     -> {
                 handleCancelSend(action)
             }
             is EventSharedAction.ReportContentSpam          -> {
-                roomDetailViewModel.handle(RoomDetailAction.ReportContent(
+                timelineViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is spam", spam = true))
             }
             is EventSharedAction.ReportContentInappropriate -> {
-                roomDetailViewModel.handle(RoomDetailAction.ReportContent(
+                timelineViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
             }
             is EventSharedAction.ReportContentCustom        -> {
@@ -2208,7 +2208,7 @@ class TimelineFragment @Inject constructor(
                 onUrlLongClicked(action.url)
             }
             is EventSharedAction.ReRequestKey               -> {
-                roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId))
+                timelineViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId))
             }
             is EventSharedAction.UseKeyBackup               -> {
                 context?.let {
@@ -2227,7 +2227,7 @@ class TimelineFragment @Inject constructor(
                 .setMessage(R.string.end_poll_confirmation_description)
                 .setNegativeButton(R.string.action_cancel, null)
                 .setPositiveButton(R.string.end_poll_confirmation_approve_button) { _, _ ->
-                    roomDetailViewModel.handle(RoomDetailAction.EndPoll(eventId))
+                    timelineViewModel.handle(RoomDetailAction.EndPoll(eventId))
                 }
                 .show()
     }
@@ -2238,7 +2238,7 @@ class TimelineFragment @Inject constructor(
                 .setMessage(R.string.room_participants_action_ignore_prompt_msg)
                 .setNegativeButton(R.string.action_cancel, null)
                 .setPositiveButton(R.string.room_participants_action_ignore) { _, _ ->
-                    roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(senderId))
+                    timelineViewModel.handle(RoomDetailAction.IgnoreUser(senderId))
                 }
                 .show()
     }
@@ -2258,7 +2258,7 @@ class TimelineFragment @Inject constructor(
             views.composerLayout.views.composerEditText.setText(Command.EMOTE.command + " ")
             views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.length)
         } else {
-            val roomMember = roomDetailViewModel.getMember(userId)
+            val roomMember = timelineViewModel.getMember(userId)
             // TODO move logic outside of fragment
             (roomMember?.displayName ?: userId)
                     .let { sanitizeDisplayName(it) }
@@ -2320,9 +2320,9 @@ class TimelineFragment @Inject constructor(
         context?.let {
             val roomThreadDetailArgs = ThreadTimelineArgs(
                     roomId = timelineArgs.roomId,
-                    displayName = roomDetailViewModel.getRoomSummary()?.displayName,
-                    avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl,
-                    roomEncryptionTrustLevel = roomDetailViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
+                    displayName = timelineViewModel.getRoomSummary()?.displayName,
+                    avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl,
+                    roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
                     rootThreadEventId = rootThreadEventId)
             navigator.openThread(it, roomThreadDetailArgs)
         }
@@ -2337,9 +2337,9 @@ class TimelineFragment @Inject constructor(
         context?.let {
             val roomThreadDetailArgs = ThreadTimelineArgs(
                     roomId = timelineArgs.roomId,
-                    displayName = roomDetailViewModel.getRoomSummary()?.displayName,
-                    roomEncryptionTrustLevel = roomDetailViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
-                    avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl)
+                    displayName = timelineViewModel.getRoomSummary()?.displayName,
+                    roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel,
+                    avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl)
             navigator.openThreadList(it, roomThreadDetailArgs)
         }
     }
@@ -2348,20 +2348,20 @@ class TimelineFragment @Inject constructor(
 
     override fun onAcceptInvite() {
         notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
-        roomDetailViewModel.handle(RoomDetailAction.AcceptInvite)
+        timelineViewModel.handle(RoomDetailAction.AcceptInvite)
     }
 
     override fun onRejectInvite() {
         notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
-        roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
+        timelineViewModel.handle(RoomDetailAction.RejectInvite)
     }
 
-    private fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
+    private fun onJumpToReadMarkerClicked() = withState(timelineViewModel) {
         if (it.unreadState is UnreadState.HasUnread) {
-            roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
+            timelineViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
         }
         if (it.unreadState is UnreadState.ReadMarkerNotLoaded) {
-            roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false))
+            timelineViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false))
         }
     }
 
@@ -2401,7 +2401,7 @@ class TimelineFragment @Inject constructor(
             AttachmentTypeSelectorView.Type.FILE    -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
             AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
             AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
-            AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+            AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
             AttachmentTypeSelectorView.Type.POLL    -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId)
         }.exhaustive
     }
@@ -2412,7 +2412,7 @@ class TimelineFragment @Inject constructor(
         val grouped = attachments.toGroupedContentAttachmentData()
         if (grouped.notPreviewables.isNotEmpty()) {
             // Send the not previewable attachments right now (?)
-            roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
+            timelineViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
         }
         if (grouped.previewables.isNotEmpty()) {
             val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
similarity index 99%
rename from vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
rename to vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index ade037b4e4..6e51fc7aa0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -103,7 +103,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
 import timber.log.Timber
 import java.util.concurrent.atomic.AtomicBoolean
 
-class RoomDetailViewModel @AssistedInject constructor(
+class TimelineViewModel @AssistedInject constructor(
         @Assisted private val initialState: RoomDetailViewState,
         private val vectorPreferences: VectorPreferences,
         private val vectorDataStore: VectorDataStore,
@@ -144,11 +144,11 @@ class RoomDetailViewModel @AssistedInject constructor(
     private var prepareToEncrypt: Async = Uninitialized
 
     @AssistedFactory
-    interface Factory : MavericksAssistedViewModelFactory {
-        override fun create(initialState: RoomDetailViewState): RoomDetailViewModel
+    interface Factory : MavericksAssistedViewModelFactory {
+        override fun create(initialState: RoomDetailViewState): TimelineViewModel
     }
 
-    companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
+    companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
         const val PAGINATION_COUNT = 50
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 9d85ec0a07..62598551de 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -215,7 +215,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             if (state.rootThreadEventId != null) {
                                 room.replyInThread(
                                         rootThreadEventId = state.rootThreadEventId,
-                                        replyInThreadText = action.text.toString(),
+                                        replyInThreadText = slashCommandResult.message,
                                         autoMarkdown = false)
                             } else {
                                 room.sendTextMessage(slashCommandResult.message, autoMarkdown = false)
@@ -270,7 +270,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                         is ParsedCommand.SendEmote                         -> {
                             state.rootThreadEventId?.let {
                                 room.replyInThread(
-                                        rootThreadEventId = state.rootThreadEventId,
+                                        rootThreadEventId = it,
                                         replyInThreadText = slashCommandResult.message,
                                         msgType = MessageType.MSGTYPE_EMOTE,
                                         autoMarkdown = action.autoMarkdown)
@@ -282,7 +282,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             val message = slashCommandResult.message.toString()
                             state.rootThreadEventId?.let {
                                 room.replyInThread(
-                                        rootThreadEventId = state.rootThreadEventId,
+                                        rootThreadEventId = it,
                                         replyInThreadText = slashCommandResult.message,
                                         formattedText = rainbowGenerator.generate(message))
                             } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message))
@@ -293,7 +293,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             val message = slashCommandResult.message.toString()
                             state.rootThreadEventId?.let {
                                 room.replyInThread(
-                                        rootThreadEventId = state.rootThreadEventId,
+                                        rootThreadEventId = it,
                                         replyInThreadText = slashCommandResult.message,
                                         msgType = MessageType.MSGTYPE_EMOTE,
                                         formattedText = rainbowGenerator.generate(message))
@@ -307,7 +307,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             val formattedText = "${slashCommandResult.message}"
                             state.rootThreadEventId?.let {
                                 room.replyInThread(
-                                        rootThreadEventId = state.rootThreadEventId,
+                                        rootThreadEventId = it,
                                         replyInThreadText = text,
                                         formattedText = formattedText)
                             } ?: room.sendFormattedTextMessage(
@@ -479,9 +479,9 @@ class MessageComposerViewModel @AssistedInject constructor(
                 }
                 is SendMode.Reply   -> {
                     val timelineEvent = state.sendMode.timelineEvent
-                    state.rootThreadEventId?.let { rootThreadEventId ->
+                    state.rootThreadEventId?.let {
                         room.replyInThread(
-                                rootThreadEventId = rootThreadEventId,
+                                rootThreadEventId = it,
                                 replyInThreadText = action.text.toString(),
                                 autoMarkdown = action.autoMarkdown,
                                 eventReplied = timelineEvent)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
index d611639463..cc2f8a2ac5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt
@@ -48,11 +48,9 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
     data class Reply(val eventId: String) :
             EventSharedAction(R.string.reply, R.drawable.ic_reply)
 
-    // TODO add translations
     data class ReplyInThread(val eventId: String) :
             EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread)
 
-    // TODO add translations
     object ViewInRoom :
             EventSharedAction(R.string.view_in_room, R.drawable.ic_thread_view_in_room_menu_item)
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index fe615d8c01..e979704d6a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -441,13 +441,12 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     }
 
     /**
-     * Determine whether or not the Reply In Thread bottom sheet setting will be visible
+     * Determine whether or not the Reply In Thread bottom sheet action will be visible
      * to the user
      */
     private fun canReplyInThread(event: TimelineEvent,
                                  messageContent: MessageContent?,
                                  actionPermissions: ActionPermissions): Boolean {
-        // Only event of type EventType.MESSAGE are supported for the moment
         if (!BuildConfig.THREADING_ENABLED) return false
         if (initialState.isFromThreadTimeline) return false
         if (event.root.getClearType() != EventType.MESSAGE &&
@@ -468,13 +467,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     }
 
     /**
-     * Determine whether or no the selected event is a root thread event from within
-     * a thread timeline
+     * Determine whether or not the view in room action will be available for the current event
      */
     private fun canViewInRoom(event: TimelineEvent,
                               messageContent: MessageContent?,
                               actionPermissions: ActionPermissions): Boolean {
-        // Only event of type EventType.MESSAGE are supported for the moment
         if (!BuildConfig.THREADING_ENABLED) return false
         if (!initialState.isFromThreadTimeline) return false
         if (event.root.getClearType() != EventType.MESSAGE &&
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt
index fafb49ad5c..a7eb6ee78f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt
@@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.views
 import android.view.View
 import android.view.ViewStub
 import im.vector.app.core.ui.views.FailedMessagesWarningView
-import im.vector.app.databinding.FragmentRoomDetailBinding
+import im.vector.app.databinding.FragmentTimelineBinding
 import im.vector.app.features.invite.VectorInviteView
 import kotlin.reflect.KMutableProperty0
 
@@ -29,12 +29,12 @@ import kotlin.reflect.KMutableProperty0
  */
 class RoomDetailLazyLoadedViews {
 
-    private var roomDetailBinding: FragmentRoomDetailBinding? = null
+    private var roomDetailBinding: FragmentTimelineBinding? = null
 
     private var failedMessagesWarningView: FailedMessagesWarningView? = null
     private var inviteView: VectorInviteView? = null
 
-    fun bind(roomDetailBinding: FragmentRoomDetailBinding) {
+    fun bind(roomDetailBinding: FragmentTimelineBinding) {
         this.roomDetailBinding = roomDetailBinding
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt
index aa6966254f..65f3d16ad4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt
@@ -30,8 +30,8 @@ import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.databinding.BottomSheetGenericListWithTitleBinding
 import im.vector.app.features.home.room.detail.RoomDetailAction
-import im.vector.app.features.home.room.detail.RoomDetailViewModel
 import im.vector.app.features.home.room.detail.RoomDetailViewState
+import im.vector.app.features.home.room.detail.TimelineViewModel
 import im.vector.app.features.navigation.Navigator
 import org.matrix.android.sdk.api.session.widgets.model.Widget
 import javax.inject.Inject
@@ -48,7 +48,7 @@ class RoomWidgetsBottomSheet :
     @Inject lateinit var colorProvider: ColorProvider
     @Inject lateinit var navigator: Navigator
 
-    private val roomDetailViewModel: RoomDetailViewModel by parentFragmentViewModel()
+    private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
 
     override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListWithTitleBinding {
         return BottomSheetGenericListWithTitleBinding.inflate(inflater, container, false)
@@ -61,7 +61,7 @@ class RoomWidgetsBottomSheet :
         views.bottomSheetTitle.textSize = 20f
         views.bottomSheetTitle.setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
         epoxyController.listener = this
-        roomDetailViewModel.onAsync(RoomDetailViewState::activeRoomWidgets) {
+        timelineViewModel.onAsync(RoomDetailViewState::activeRoomWidgets) {
             epoxyController.setData(it)
         }
     }
@@ -72,13 +72,13 @@ class RoomWidgetsBottomSheet :
         super.onDestroyView()
     }
 
-    override fun didSelectWidget(widget: Widget) = withState(roomDetailViewModel) {
+    override fun didSelectWidget(widget: Widget) = withState(timelineViewModel) {
         navigator.openRoomWidget(requireContext(), it.roomId, widget)
         dismiss()
     }
 
     override fun didSelectManageWidgets() {
-        roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
+        timelineViewModel.handle(RoomDetailAction.OpenIntegrationManager)
         dismiss()
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt
similarity index 96%
rename from vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
rename to vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt
index 72ba673972..2364e86166 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/model/ThreadListItem.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 New Vector Ltd
+ * Copyright 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.
@@ -36,8 +36,8 @@ import im.vector.app.features.home.AvatarRenderer
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.util.MatrixItem
 
-@EpoxyModelClass(layout = R.layout.item_thread_list)
-abstract class ThreadListModel : VectorEpoxyModel() {
+@EpoxyModelClass(layout = R.layout.item_thread)
+abstract class ThreadListItem : VectorEpoxyModel() {
 
     @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
     @EpoxyAttribute lateinit var matrixItem: MatrixItem
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
index 984c8e8f7e..c123bceafb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt
@@ -22,7 +22,7 @@ import im.vector.app.core.date.DateFormatKind
 import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.home.AvatarRenderer
-import im.vector.app.features.home.room.threads.list.model.threadList
+import im.vector.app.features.home.room.threads.list.model.threadListItem
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.api.util.toMatrixItem
@@ -60,7 +60,7 @@ class ThreadListController @Inject constructor(
                 ?.forEach { timelineEvent ->
                     val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST)
                     val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
-                    threadList {
+                    threadListItem {
                         id(timelineEvent.eventId)
                         avatarRenderer(host.avatarRenderer)
                         matrixItem(timelineEvent.senderInfo.toMatrixItem())
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
index 2a14ce3aa0..f388ce1410 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt
@@ -75,6 +75,7 @@ class ThreadListFragment @Inject constructor(
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         initToolbar()
+        initTextConstants()
         views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false)
         threadListController.listener = this
     }
@@ -90,6 +91,12 @@ class ThreadListFragment @Inject constructor(
         renderToolbar()
     }
 
+    private fun initTextConstants() {
+        views.threadListEmptyNoticeTextView.text = String.format(
+                resources.getString(R.string.thread_list_empty_notice),
+                resources.getString(R.string.reply_in_thread))
+    }
+
     override fun invalidate() = withState(threadListViewModel) { state ->
         renderEmptyStateIfNeeded(state)
         threadListController.update(state)
diff --git a/vector/src/main/res/layout/fragment_thread_list.xml b/vector/src/main/res/layout/fragment_thread_list.xml
index 77f46bf3ee..7e7c79f8c3 100644
--- a/vector/src/main/res/layout/fragment_thread_list.xml
+++ b/vector/src/main/res/layout/fragment_thread_list.xml
@@ -34,7 +34,7 @@
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/threadListAppBarLayout"
-        tools:listitem="@layout/item_thread_list"
+        tools:listitem="@layout/item_thread"
         tools:visibility="gone" />
 
     
+            tools:text="@string/thread_list_empty_notice" />
 
 
     
diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_timeline.xml
similarity index 100%
rename from vector/src/main/res/layout/fragment_room_detail.xml
rename to vector/src/main/res/layout/fragment_timeline.xml
diff --git a/vector/src/main/res/layout/item_thread_list.xml b/vector/src/main/res/layout/item_thread.xml
similarity index 100%
rename from vector/src/main/res/layout/item_thread_list.xml
rename to vector/src/main/res/layout/item_thread.xml
diff --git a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
index 6a72422e20..e16912246e 100644
--- a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
+++ b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml
@@ -56,7 +56,7 @@
         app:layout_constraintBottom_toBottomOf="@id/roomToolbarThreadImageView"
         app:layout_constraintStart_toEndOf="@id/roomToolbarThreadImageView"
         app:layout_constraintTop_toTopOf="@id/roomToolbarThreadImageView"
-        tools:text="RoomName"
+        tools:text="@sample/rooms.json/data/name"
         tools:visibility="visible" />
 
 
diff --git a/vector/src/main/res/layout/view_thread_room_summary.xml b/vector/src/main/res/layout/view_thread_room_summary.xml
index b1c490e5b3..0f184edef3 100644
--- a/vector/src/main/res/layout/view_thread_room_summary.xml
+++ b/vector/src/main/res/layout/view_thread_room_summary.xml
@@ -55,5 +55,5 @@
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView"
         app:layout_constraintTop_toTopOf="parent"
-        tools:text="Hello There, whats up! Its a large sentence whats up! Its a large centence" />
+        tools:text="@sample/messages.json/data/message" />
 
diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml
index 10f4fb0575..962c505e4e 100644
--- a/vector/src/main/res/menu/menu_timeline.xml
+++ b/vector/src/main/res/menu/menu_timeline.xml
@@ -90,7 +90,7 @@
             
         
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index b86b2dd27f..4e3b132be7 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -472,7 +472,6 @@
     
     View in room
     Copy link to thread
-    Share
 
     
     Confirmation
@@ -1048,7 +1047,8 @@
     Shows all threads you’ve participated in
     Keep discussions organised with threads
     Threads help keep your conversations on-topic and easy to track.
-    Tip: Long tap a message and use “Reply in thread”.
+    
+    Tip: Long tap a message and use “%s”.
 
     
     Reason for reporting this content

From 5e23947419e7a90d448c43474e3aaaa8c6de4674 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 17 Jan 2022 19:22:22 +0200
Subject: [PATCH 096/130] Enhance filtering to support threads

---
 .../sdk/internal/session/search/SearchTask.kt | 35 ++++++++++-
 .../detail/search/SearchResultController.kt   |  1 +
 .../room/detail/search/SearchResultItem.kt    | 41 ++++++++++++
 .../main/res/layout/item_search_result.xml    | 62 +++++++++++++++++++
 vector/src/main/res/values/strings.xml        |  1 +
 5 files changed, 138 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
index 8de762ee1b..3ba7d11c3d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt
@@ -19,6 +19,10 @@ package org.matrix.android.sdk.internal.session.search
 import org.matrix.android.sdk.api.session.search.EventAndSender
 import org.matrix.android.sdk.api.session.search.SearchResult
 import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestBody
@@ -28,6 +32,7 @@ import org.matrix.android.sdk.internal.session.search.request.SearchRequestFilte
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestOrder
 import org.matrix.android.sdk.internal.session.search.request.SearchRequestRoomEvents
 import org.matrix.android.sdk.internal.session.search.response.SearchResponse
+import org.matrix.android.sdk.internal.session.search.response.SearchResponseItem
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
@@ -47,7 +52,8 @@ internal interface SearchTask : Task {
 
 internal class DefaultSearchTask @Inject constructor(
         private val searchAPI: SearchAPI,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val realmSessionProvider: RealmSessionProvider
 ) : SearchTask {
 
     override suspend fun execute(params: SearchTask.Params): SearchResult {
@@ -74,12 +80,22 @@ internal class DefaultSearchTask @Inject constructor(
     }
 
     private fun SearchResponse.toDomain(): SearchResult {
+        val localTimelineEvents = findRootThreadEventsFromDB(searchCategories.roomEvents?.results)
         return SearchResult(
                 nextBatch = searchCategories.roomEvents?.nextBatch,
                 highlights = searchCategories.roomEvents?.highlights,
                 results = searchCategories.roomEvents?.results?.map { searchResponseItem ->
+
+                    val localThreadEventDetails = localTimelineEvents
+                            ?.firstOrNull { it.eventId ==  searchResponseItem.event.eventId }
+                            ?.root
+                            ?.asDomain()
+                            ?.threadDetails
+
                     EventAndSender(
-                            searchResponseItem.event,
+                            searchResponseItem.event.apply {
+                                threadDetails = localThreadEventDetails
+                            },
                             searchResponseItem.event.senderId?.let { senderId ->
                                 searchResponseItem.context?.profileInfo?.get(senderId)
                                         ?.let {
@@ -94,4 +110,19 @@ internal class DefaultSearchTask @Inject constructor(
                 }?.reversed()
         )
     }
+
+    /**
+     * Find local events if exists in order to enhance the result with thread summary
+     */
+    private fun findRootThreadEventsFromDB(searchResponseItemList: List?): List? {
+        return realmSessionProvider.withRealm { realm ->
+            searchResponseItemList?.mapNotNull {
+                it.event.roomId ?: return@mapNotNull null
+                it.event.eventId ?: return@mapNotNull null
+                TimelineEventEntity.where(realm, it.event.roomId, it.event.eventId).findFirst()
+            }?.filter {
+                it.root?.isRootThread == true || it.root?.isThread() == true
+            }
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
index 4c5a52864d..5c0f47a452 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
@@ -122,6 +122,7 @@ class SearchResultController @Inject constructor(
                     .spannable(spannable.toEpoxyCharSequence())
                     .sender(eventAndSender.sender
                             ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
+                    .threadDetails(event.threadDetails)
                     .listener { listener?.onItemClicked(eventAndSender.event) }
                     .let { result.add(it) }
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
index 9d146792d9..fcddc2286b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
@@ -18,8 +18,11 @@ package im.vector.app.features.home.room.detail.search
 
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.BuildConfig
 import im.vector.app.R
 import im.vector.app.core.epoxy.ClickListener
 import im.vector.app.core.epoxy.VectorEpoxyHolder
@@ -29,6 +32,7 @@ import im.vector.app.core.epoxy.onClick
 import im.vector.app.core.extensions.setTextOrHide
 import im.vector.app.features.displayname.getBestName
 import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.session.threads.ThreadDetails
 import org.matrix.android.sdk.api.util.MatrixItem
 
 @EpoxyModelClass(layout = R.layout.item_search_result)
@@ -38,6 +42,8 @@ abstract class SearchResultItem : VectorEpoxyModel() {
     @EpoxyAttribute var formattedDate: String? = null
     @EpoxyAttribute lateinit var spannable: EpoxyCharSequence
     @EpoxyAttribute var sender: MatrixItem? = null
+    @EpoxyAttribute var threadDetails: ThreadDetails? = null
+
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
 
     override fun bind(holder: Holder) {
@@ -48,6 +54,36 @@ abstract class SearchResultItem : VectorEpoxyModel() {
         holder.memberNameView.setTextOrHide(sender?.getBestName())
         holder.timeView.text = formattedDate
         holder.contentView.text = spannable.charSequence
+
+        if (BuildConfig.THREADING_ENABLED) {
+            threadDetails?.let {
+                if (it.isRootThread) {
+                    showThreadSummary(holder)
+                    holder.threadSummaryCounterTextView.text = it.numberOfThreads.toString()
+                    holder.threadSummaryInfoTextView.text = it.threadSummaryLatestTextMessage.orEmpty()
+
+                    val userId = it.threadSummarySenderInfo?.userId ?: return@let
+                    val displayName = it.threadSummarySenderInfo?.displayName
+                    val avatarUrl = it.threadSummarySenderInfo?.avatarUrl
+                    avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView)
+                } else {
+                    showFromThread(holder)
+                }
+            } ?: run {
+                holder.threadSummaryConstraintLayout.isVisible = false
+                holder.fromThreadConstraintLayout.isVisible = false
+            }
+        }
+    }
+
+    private fun showThreadSummary(holder: Holder, show: Boolean = true) {
+        holder.threadSummaryConstraintLayout.isVisible = show
+        holder.fromThreadConstraintLayout.isVisible = !show
+    }
+
+    private fun showFromThread(holder: Holder, show: Boolean = true) {
+        holder.threadSummaryConstraintLayout.isVisible = !show
+        holder.fromThreadConstraintLayout.isVisible = show
     }
 
     class Holder : VectorEpoxyHolder() {
@@ -55,5 +91,10 @@ abstract class SearchResultItem : VectorEpoxyModel() {
         val memberNameView by bind(R.id.messageMemberNameView)
         val timeView by bind(R.id.messageTimeView)
         val contentView by bind(R.id.messageContentView)
+        val threadSummaryConstraintLayout by bind(R.id.searchThreadSummaryConstraintLayout)
+        val threadSummaryCounterTextView by bind(R.id.messageThreadSummaryCounterTextView)
+        val threadSummaryAvatarImageView by bind(R.id.messageThreadSummaryAvatarImageView)
+        val threadSummaryInfoTextView by bind(R.id.messageThreadSummaryInfoTextView)
+        val fromThreadConstraintLayout by bind(R.id.searchFromThreadConstraintLayout)
     }
 }
diff --git a/vector/src/main/res/layout/item_search_result.xml b/vector/src/main/res/layout/item_search_result.xml
index bdd1294b88..3264a7d230 100644
--- a/vector/src/main/res/layout/item_search_result.xml
+++ b/vector/src/main/res/layout/item_search_result.xml
@@ -62,4 +62,66 @@
         app:layout_constraintTop_toBottomOf="@id/messageMemberNameView"
         tools:text="@sample/messages.json/data/message" />
 
+    
+
+        
+    
+
+    
+
+        
+
+        
+
+    
 
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 4e3b132be7..f6868adc34 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -1049,6 +1049,7 @@
     Threads help keep your conversations on-topic and easy to track.
     
     Tip: Long tap a message and use “%s”.
+    From a Thread
 
     
     Reason for reporting this content

From 10599aa7286a86105943336dd29823e6b1034682 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 18 Jan 2022 13:11:52 +0200
Subject: [PATCH 097/130] ktlint format

---
 .../src/main/java/org/matrix/android/sdk/rx/RxRoom.kt           | 0
 .../home/room/detail/composer/MessageComposerViewModel.kt       | 2 +-
 2 files changed, 1 insertion(+), 1 deletion(-)
 delete mode 100644 matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt

diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 60622b3fe7..00755a78e7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -183,7 +183,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         withState { state ->
             when (state.sendMode) {
                 is SendMode.Regular -> {
-                    when (val slashCommandResult = CommandParser.parseSplashCommand(
+                    when (val slashCommandResult = CommandParser.parseSlashCommand(
                             textMessage = action.text,
                             isInThreadTimeline =  state.isInThreadTimeline())) {
                         is ParsedCommand.ErrorNotACommand                  -> {

From 707397cb9d41a331b83d0a8b02c836aa467d0f1f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 18 Jan 2022 15:28:44 +0200
Subject: [PATCH 098/130] cleanup

---
 .../room/relation/DefaultRelationService.kt        |  2 ++
 .../session/room/send/LocalEchoEventFactory.kt     |  8 ++++++++
 .../session/room/timeline/LoadTimelineStrategy.kt  |  1 -
 .../session/room/timeline/TimelineChunk.kt         | 14 +-------------
 .../sync/handler/room/ThreadsAwarenessHandler.kt   |  2 +-
 .../java/im/vector/app/features/command/Command.kt |  4 ++--
 6 files changed, 14 insertions(+), 17 deletions(-)

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 47794e424f..b5b9aa5afb 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
@@ -176,6 +176,7 @@ internal class DefaultRelationService @AssistedInject constructor(
             formattedText: String?,
             eventReplied: TimelineEvent?): Cancelable? {
         val event = if (eventReplied != null) {
+            // Reply within a thread
             eventFactory.createReplyTextEvent(
                     roomId = roomId,
                     eventReplied = eventReplied,
@@ -187,6 +188,7 @@ internal class DefaultRelationService @AssistedInject constructor(
                     }
                     ?: return null
         } else {
+            // Normal thread reply
             eventFactory.createThreadTextEvent(
                     rootThreadEventId = rootThreadEventId,
                     roomId = 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 86da186222..53ea19e761 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
@@ -457,6 +457,14 @@ internal class LocalEchoEventFactory @Inject constructor(
     /**
      * Generates the appropriate relatesTo object for a reply event.
      * It can either be a regular reply or a reply within a thread
+     * "m.relates_to": {
+     *      "rel_type": "m.thread",
+     *      "event_id": "$thread_root",
+     *      "m.in_reply_to": {
+     *          "event_id": "$event_target",
+     *          "render_in": ["m.thread"]
+     *        }
+     *   }
      */
     private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null): RelationDefaultContent =
             rootThreadEventId?.let {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index 1d92d89a3a..efc11a8bde 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -171,7 +171,6 @@ internal class LoadTimelineStrategy(
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
-        // /
         if (mode is Mode.Permalink && timelineChunk == null) {
             val params = GetContextOfEventTask.Params(roomId, mode.originEventId)
             try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 20942de408..33f8d88d73 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -289,22 +289,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val displayIndex = getNextDisplayIndex(direction) ?: return LoadedFromStorage()
         val baseQuery = timelineEventEntities.where()
 
-//        val timelineEvents = if (timelineSettings.rootThreadEventId != null) {
-//            baseQuery
-//                    .beginGroup()
-//                    .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, timelineSettings.rootThreadEventId)
-//                    .or()
-//                    .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, timelineSettings.rootThreadEventId)
-//                    .endGroup()
-//                    .offsets(direction, count, displayIndex)
-//                    .findAll()
-//                    .orEmpty()
-//        } else {
         val timelineEvents = baseQuery
                 .offsets(direction, count, displayIndex)
                 .findAll()
                 .orEmpty()
-//        }
 
         if (timelineEvents.isEmpty()) return LoadedFromStorage()
         fetchRootThreadEventsIfNeeded(timelineEvents)
@@ -331,7 +319,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
     }
 
     /**
-     * Returns whether or not the the thread has reached end. It returned false if the current timeline
+     * Returns whether or not the the thread has reached end. It returns false if the current timeline
      * is not a thread timeline
      */
     private fun threadReachedEnd(timelineEvents: List): Boolean =
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index ee606d1fac..d093aab21f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -171,7 +171,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     /**
      * If the event is a thread event then transform/enhance it to a visual Reply Event,
      * If the event is not a thread event, null value will be returned
-     * If there is an error (ex. the root/origin thread event is not found), null willl be returend
+     * If there is an error (ex. the root/origin thread event is not found), null will be returned
      */
     private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? {
         roomId ?: return null
diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt
index 5725703c7e..57f228e75d 100644
--- a/vector/src/main/java/im/vector/app/features/command/Command.kt
+++ b/vector/src/main/java/im/vector/app/features/command/Command.kt
@@ -45,8 +45,8 @@ enum class Command(val command: String,
     REMOVE_USER("/remove", arrayOf("/kick"), " [reason]", R.string.command_description_kick_user, false, false),
     CHANGE_DISPLAY_NAME("/nick", null, "", R.string.command_description_nick, false, false),
     CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", arrayOf("/roomnick"), "", R.string.command_description_nick_for_room, false, false),
-    ROOM_AVATAR("/roomavatar", null, "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */, false),
-    CHANGE_AVATAR_FOR_ROOM("/myroomavatar", null, "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */, false),
+    ROOM_AVATAR("/roomavatar", null, "", R.string.command_description_room_avatar, true /* User has to know the mxc url */, false),
+    CHANGE_AVATAR_FOR_ROOM("/myroomavatar", null, "", R.string.command_description_avatar_for_room, true /* User has to know the mxc url */, false),
     MARKDOWN("/markdown", null, "", R.string.command_description_markdown, false, false),
     RAINBOW("/rainbow", null, "", R.string.command_description_rainbow, false, true),
     RAINBOW_EMOTE("/rainbowme", null, "", R.string.command_description_rainbow_emote, false, true),

From 4cff3938e7e1f0d9d8ee235ffa7c03cf1e6d4a68 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Tue, 18 Jan 2022 16:05:41 +0200
Subject: [PATCH 099/130] - Hide read receipts from thread timeline - Enhance
 FetchThreadTimelineTask

---
 .../room/model/relation/RelationService.kt    |   2 +-
 .../database/helper/ThreadEventsHelper.kt     |   9 +-
 .../room/relation/DefaultRelationService.kt   |   2 +-
 .../threads/FetchThreadTimelineTask.kt        | 165 +++++++++++++++++-
 .../timeline/TimelineEventController.kt       |   7 +-
 .../factory/ReadReceiptsItemFactory.kt        |   7 +-
 .../detail/timeline/item/ReadReceiptsItem.kt  |   3 +
 7 files changed, 185 insertions(+), 10 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 183cd481d2..e49b1f0a73 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
@@ -153,5 +153,5 @@ interface RelationService {
      * from the backend
      * @param rootThreadEventId the root thread eventId
      */
-    suspend fun fetchThreadTimeline(rootThreadEventId: String): List
+    suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 557bb4bdf1..57e65941e7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -36,7 +36,10 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId
  * Finds the root thread event and update it with the latest message summary along with the number
  * of threads included. If there is no root thread event no action is done
  */
-internal fun Map.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) {
+internal fun Map.updateThreadSummaryIfNeeded(
+        roomId: String,
+        realm: Realm, currentUserId: String,
+        shouldUpdateNotifications: Boolean = true) {
     if (!BuildConfig.THREADING_ENABLED) return
 
     for ((rootThreadEventId, eventEntity) in this) {
@@ -55,7 +58,9 @@ internal fun Map.updateThreadSummaryIfNeeded(roomId: String
         }
     }
 
-    updateNotificationsNew(roomId, realm, currentUserId)
+    if(shouldUpdateNotifications) {
+        updateNotificationsNew(roomId, realm, currentUserId)
+    }
 }
 
 /**
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 b5b9aa5afb..eee553ab80 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
@@ -203,7 +203,7 @@ internal class DefaultRelationService @AssistedInject constructor(
         return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
-    override suspend fun fetchThreadTimeline(rootThreadEventId: String): List {
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
         return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index d62ce4158f..f50a691d43 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -15,17 +15,45 @@
  */
 package org.matrix.android.sdk.internal.session.room.relation.threads
 
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+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.RelationType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
+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.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+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
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
 import javax.inject.Inject
 
-internal interface FetchThreadTimelineTask : Task> {
+internal interface FetchThreadTimelineTask : Task {
     data class Params(
             val roomId: String,
             val rootThreadEventId: String
@@ -35,10 +63,13 @@ internal interface FetchThreadTimelineTask : Task {
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
         val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
         val response = executeRequest(globalErrorReceiver) {
             roomAPI.getRelations(
@@ -50,6 +81,132 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
             )
         }
 
-        return response.chunks + listOfNotNull(response.originalEvent)
+        val threadList = response.chunks + listOfNotNull(response.originalEvent)
+
+
+        return storeNewEventsIfNeeded(threadList, params.roomId)
+    }
+
+
+    /**
+     * Store new events if they are not already received, and returns weather or not,
+     * a timeline update should be made
+     * @param threadList is the list containing the thread replies
+     * @param roomId the roomId of the the thread
+     * @return
+     */
+    private suspend fun storeNewEventsIfNeeded(threadList: List, roomId: String): Boolean {
+        var eventsSkipped = 0
+        monarchy
+                .awaitTransaction { realm ->
+                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
+
+                    val optimizedThreadSummaryMap = hashMapOf()
+                    val roomMemberContentsByUser = HashMap()
+
+                    for (event in threadList.reversed()) {
+
+                        if (event.eventId == null || event.senderId == null || event.type == null) {
+                            eventsSkipped++
+                            continue
+                        }
+
+                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
+                            //  Skip if event already exists
+                            eventsSkipped++
+                            continue
+                        }
+                        if (event.isEncrypted()) {
+                            // Decrypt events that will be stored
+                            decryptIfNeeded(event, roomId)
+                        }
+
+                        handleReaction(realm, event, roomId)
+
+                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+
+                        // Sender info
+                        roomMemberContentsByUser.getOrPut(event.senderId) {
+                            // If we don't have any new state on this user, get it from db
+                            val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
+                            rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
+                        }
+
+                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+                        eventEntity.rootThreadEventId?.let {
+                            // This is a thread event
+                            optimizedThreadSummaryMap[it] = eventEntity
+                        } ?: run {
+                            // This is a normal event or a root thread one
+                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+                        }
+                    }
+
+                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                            roomId = roomId,
+                            realm = realm,
+                            currentUserId = userId,
+                            shouldUpdateNotifications = false
+                    )
+                }
+        Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
+
+        return eventsSkipped == threadList.size
+    }
+
+    /**
+     * Invoke the event decryption mechanism for a specific event
+     */
+
+    private fun decryptIfNeeded(event: Event, roomId: String) {
+        try {
+            // Event from sync does not have roomId, so add it to the event first
+            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
+            event.mxDecryptionResult = OlmDecryptionResult(
+                    payload = result.clearEvent,
+                    senderKey = result.senderCurve25519Key,
+                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+            )
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+
+    private fun handleReaction(realm: Realm,
+                               event: Event,
+                               roomId: String) {
+
+        val unsignedData = event.unsignedData ?: return
+        val relatedEventId = event.eventId ?: return
+
+        unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
+
+            if (relationChunk.type == EventType.REACTION) {
+                val reaction = relationChunk.key
+                Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
+
+                val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
+                var sum = eventSummary.reactionsSummary.find { it.key == reaction }
+
+                if (sum == null) {
+                    sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
+                    sum.key = reaction
+                    sum.firstTimestamp = event.originServerTs ?: 0
+                    Timber.v("Adding synced reaction $reaction")
+                    sum.count = 1
+                    // reactionEventId not included in the /relations API
+//                    sum.sourceEvents.add(reactionEventId)
+                    eventSummary.reactionsSummary.add(sum)
+                } else {
+                    sum.count += 1
+                }
+            }
+
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index bd181d71ba..b6b985632c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -443,7 +443,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         }
         val readReceipts = receiptsByEvents[event.eventId].orEmpty()
         return copy(
-                readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
+                readReceiptsItem = readReceiptsItemFactory.create(
+                        event.eventId,
+                        readReceipts,
+                        callback,
+                        partialState.isFromThreadTimeline()
+                ),
                 formattedDayModel = formattedDayModel,
                 mergedHeaderModel = mergedHeaderModel
         )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
index 8a74a6d207..d477a3d40e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
@@ -26,7 +26,11 @@ import javax.inject.Inject
 
 class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
 
-    fun create(eventId: String, readReceipts: List, callback: TimelineEventController.Callback?): ReadReceiptsItem? {
+    fun create(
+            eventId: String,
+            readReceipts: List,
+            callback: TimelineEventController.Callback?,
+            isFromThreadTimeLine: Boolean): ReadReceiptsItem? {
         if (readReceipts.isEmpty()) {
             return null
         }
@@ -41,6 +45,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
                 .eventId(eventId)
                 .readReceipts(readReceiptsData)
                 .avatarRenderer(avatarRenderer)
+                .shouldHideReadReceipts(isFromThreadTimeLine)
                 .clickListener {
                     callback?.onReadReceiptsClicked(readReceiptsData)
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt
index 650c804cfa..4f29253264 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.home.room.detail.timeline.item
 
+import androidx.core.view.isVisible
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import com.airbnb.epoxy.EpoxyModelWithHolder
@@ -31,6 +32,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder(
 
     @EpoxyAttribute lateinit var eventId: String
     @EpoxyAttribute lateinit var readReceipts: List
+    @EpoxyAttribute var shouldHideReadReceipts: Boolean = false
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
 
@@ -42,6 +44,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder(
         super.bind(holder)
         holder.readReceiptsView.onClick(clickListener)
         holder.readReceiptsView.render(readReceipts, avatarRenderer)
+        holder.readReceiptsView.isVisible = !shouldHideReadReceipts
     }
 
     override fun unbind(holder: Holder) {

From 8cc96e27bcd80a1da1b22249c5f600a779a4194b Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 19 Jan 2022 12:28:00 +0200
Subject: [PATCH 100/130] - Add threads to lab settings - Disable thread
 awareness due to the new fallback mechanism

---
 .../java/org/matrix/android/sdk/api/Matrix.kt |  5 +++
 .../database/helper/ThreadEventsHelper.kt     |  1 -
 .../session/room/timeline/TimelineChunk.kt    |  5 ++-
 .../room/timeline/TimelineEventDecryptor.kt   | 17 ++++----
 .../room/timeline/TokenChunkEventPersistor.kt | 21 ++++++----
 .../session/sync/SyncResponseHandler.kt       |  7 ++--
 .../sync/handler/room/RoomSyncHandler.kt      | 42 +++++++++++--------
 .../app/core/platform/VectorBaseActivity.kt   |  2 +
 .../core/resources/UserPreferencesProvider.kt |  4 ++
 .../command/AutocompleteCommandPresenter.kt   |  2 +-
 .../app/features/command/CommandParser.kt     |  2 +-
 .../home/room/detail/TimelineFragment.kt      |  2 +-
 .../home/room/detail/TimelineViewModel.kt     |  2 +-
 .../detail/search/SearchResultController.kt   |  5 ++-
 .../room/detail/search/SearchResultItem.kt    |  3 +-
 .../action/MessageActionsViewModel.kt         |  4 +-
 .../helper/MessageItemAttributesFactory.kt    |  5 ++-
 .../helper/TimelineEventVisibilityHelper.kt   |  5 +--
 .../detail/timeline/item/AbsMessageItem.kt    |  5 ++-
 .../features/settings/VectorPreferences.kt    |  5 +++
 .../settings/VectorSettingsLabsFragment.kt    | 13 ++++++
 vector/src/main/res/values/strings.xml        |  2 +
 .../src/main/res/xml/vector_settings_labs.xml | 15 ++++---
 23 files changed, 116 insertions(+), 58 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 901ba75d16..f903898398 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -96,6 +96,11 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
 
     companion object {
 
+        /**
+         * Determines whether or not thread messages are enabled
+         */
+        var areThreadMessagesEnabled: Boolean = false
+
         private lateinit var instance: Matrix
         private val isInit = AtomicBoolean(false)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index 57e65941e7..71be1f3a4a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -40,7 +40,6 @@ internal fun Map.updateThreadSummaryIfNeeded(
         roomId: String,
         realm: Realm, currentUserId: String,
         shouldUpdateNotifications: Boolean = true) {
-    if (!BuildConfig.THREADING_ENABLED) return
 
     for ((rootThreadEventId, eventEntity) in this) {
         eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 33f8d88d73..9db9fe7c93 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -295,7 +295,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 .orEmpty()
 
         if (timelineEvents.isEmpty()) return LoadedFromStorage()
-        fetchRootThreadEventsIfNeeded(timelineEvents)
+// Disabled due to the new fallback
+//        fetchRootThreadEventsIfNeeded(timelineEvents)
         if (direction == Timeline.Direction.FORWARDS) {
             builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
         }
@@ -332,7 +333,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * in order to be able to display the event to the user appropriately
      */
     private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List) {
-        if (BuildConfig.THREADING_ENABLED) return
+//        if (BuildConfig.THREADING_ENABLED) return
         val eventEntityList = offsetResults
                 .mapNotNull {
                     it.root
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index a4d48903ad..5913aa6e7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -116,15 +116,16 @@ internal class TimelineEventDecryptor @Inject constructor(
 
                 eventEntity?.apply {
                     val decryptedPayload =
-                            if (!BuildConfig.THREADING_ENABLED) {
-                                threadsAwarenessHandler.handleIfNeededDuringDecryption(
-                                        it,
-                                        roomId = event.roomId,
-                                        event,
-                                        result)
-                            } else {
+// Disabled due to the new fallback
+//                            if (!BuildConfig.THREADING_ENABLED) {
+//                                threadsAwarenessHandler.handleIfNeededDuringDecryption(
+//                                        it,
+//                                        roomId = event.roomId,
+//                                        event,
+//                                        result)
+//                            } else {
                                 null
-                            }
+//                            }
                     setDecryptionResult(result, decryptedPayload)
                 }
             }
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 5115c852ca..bf7ebbec54 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
@@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.session.room.timeline
 import com.zhuinden.monarchy.Monarchy
 import dagger.Lazy
 import io.realm.Realm
+import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@@ -50,6 +52,7 @@ import javax.inject.Inject
 internal class TokenChunkEventPersistor @Inject constructor(
                                                             @SessionDatabase private val monarchy: Monarchy,
                                                             @UserId private val userId: String,
+                                                            private val matrixConfiguration: MatrixConfiguration,
                                                             private val liveEventManager: Lazy) {
 
     enum class Result {
@@ -182,18 +185,22 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
                 currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
-                eventEntity.rootThreadEventId?.let {
-                    // This is a thread event
-                    optimizedThreadSummaryMap[it] = eventEntity
-                } ?: run {
-                    // This is a normal event or a root thread one
-                    optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+                if(Matrix.areThreadMessagesEnabled) {
+                    eventEntity.rootThreadEventId?.let {
+                        // This is a thread event
+                        optimizedThreadSummaryMap[it] = eventEntity
+                    } ?: run {
+                        // This is a normal event or a root thread one
+                        optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+                    }
                 }
             }
         }
         if (currentChunk.isValid) {
             RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
         }
-        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
+        if(Matrix.areThreadMessagesEnabled) {
+            optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
+        }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index a3cfddd472..cfe021f08c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -102,9 +102,10 @@ internal class SyncResponseHandler @Inject constructor(
         val aggregator = SyncResponsePostTreatmentAggregator()
 
         // Prerequisite for thread events handling in RoomSyncHandler
-        if (!BuildConfig.THREADING_ENABLED) {
-            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
-        }
+// Disabled due to the new fallback
+//        if (!BuildConfig.THREADING_ENABLED) {
+//            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+//        }
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
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 5b6e2a9428..cf0cdc1015 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
@@ -19,7 +19,8 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 import dagger.Lazy
 import io.realm.Realm
 import io.realm.kotlin.createObject
-import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.MatrixConfiguration
 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
@@ -82,6 +83,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomMemberEventHandler: RoomMemberEventHandler,
                                                    private val roomTypingUsersHandler: RoomTypingUsersHandler,
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+                                                   private val matrixConfiguration: MatrixConfiguration,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
                                                    private val timelineInput: TimelineInput,
@@ -379,13 +381,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             if (event.isEncrypted() && !isInitialSync) {
                 decryptIfNeeded(event, roomId)
             }
-
-            if (!BuildConfig.THREADING_ENABLED) {
-                threadsAwarenessHandler.handleIfNeeded(
-                        realm = realm,
-                        roomId = roomId,
-                        event = event)
-            }
+// Disabled due to the new fallback
+//            if (!BuildConfig.THREADING_ENABLED) {
+//                threadsAwarenessHandler.handleIfNeeded(
+//                        realm = realm,
+//                        roomId = roomId,
+//                        event = event)
+//            }
 
             val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
@@ -408,12 +410,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
 
             chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
-            eventEntity.rootThreadEventId?.let {
-                // This is a thread event
-                optimizedThreadSummaryMap[it] = eventEntity
-            } ?: run {
-                // This is a normal event or a root thread one
-                optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+            if(Matrix.areThreadMessagesEnabled) {
+                eventEntity.rootThreadEventId?.let {
+                    // This is a thread event
+                    optimizedThreadSummaryMap[it] = eventEntity
+                } ?: run {
+                    // This is a normal event or a root thread one
+                    optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
+                }
             }
 
             // Give info to crypto module
@@ -442,10 +446,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
         // Handle deletion of [stuck] local echos if needed
         deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
-        optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
-                roomId = roomId,
-                realm = realm,
-                currentUserId = userId)
+        if(Matrix.areThreadMessagesEnabled) {
+            optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
+                    roomId = roomId,
+                    realm = realm,
+                    currentUserId = userId)
+        }
 
         // posting new events to timeline if any is registered
         timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index 21419d55cf..67463a2ead 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -83,6 +83,7 @@ import im.vector.app.features.themes.ThemeUtils
 import im.vector.app.receivers.DebugReceiver
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import org.matrix.android.sdk.api.Matrix
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.GlobalError
 import reactivecircus.flowbinding.android.view.clicks
@@ -193,6 +194,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
         navigator = singletonEntryPoint.navigator()
         activeSessionHolder = singletonEntryPoint.activeSessionHolder()
         vectorPreferences = singletonEntryPoint.vectorPreferences()
+        Matrix.areThreadMessagesEnabled = vectorPreferences.areThreadMessagesEnabled()
         configurationViewModel.activityRestarter.observe(this) {
             if (!it.hasBeenHandled) {
                 // Recreate the Activity because configuration has changed
diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
index e7cabd1540..77e773c781 100644
--- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
@@ -52,4 +52,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
     fun shouldShowPolls(): Boolean {
         return vectorPreferences.labsEnablePolls()
     }
+
+    fun areThreadMessagesEnabled(): Boolean {
+        return vectorPreferences.areThreadMessagesEnabled()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
index 4e4f1fef8a..85f0a8ba2d 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt
@@ -57,7 +57,7 @@ class AutocompleteCommandPresenter @AssistedInject constructor(
                     !it.isDevCommand || vectorPreferences.developerMode()
                 }
                 .filter {
-                    if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
+                    if (vectorPreferences.areThreadMessagesEnabled() && isInThreadTimeline) {
                         it.isThreadCommand
                     } else {
                         true
diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
index e76428fb1e..983a094fa3 100644
--- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt
@@ -65,7 +65,7 @@ object CommandParser {
             val slashCommand = messageParts.first()
             val message = textMessage.substring(slashCommand.length).trim()
 
-            if (BuildConfig.THREADING_ENABLED && isInThreadTimeline) {
+            if (isInThreadTimeline) {
                 val notSupportedCommandsInThreads = Command.values().filter {
                     !it.isThreadCommand
                 }.map {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index f9e2535205..1f0f796cf7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1958,7 +1958,7 @@ class TimelineFragment @Inject constructor(
     }
 
     override fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) {
-        if (BuildConfig.THREADING_ENABLED && isRootThreadEvent && !isThreadTimeLine()) {
+        if (vectorPreferences.areThreadMessagesEnabled() && isRootThreadEvent && !isThreadTimeLine()) {
             navigateToThreadTimeline(eventId)
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 69d17e448a..d051860f32 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -695,7 +695,7 @@ class TimelineViewModel @AssistedInject constructor(
                 // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
                 R.id.join_conference           -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
                 R.id.search                    -> true
-                R.id.menu_timeline_thread_list -> BuildConfig.THREADING_ENABLED
+                R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
                 R.id.dev_tools                 -> vectorPreferences.developerMode()
                 else                           -> false
             }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
index 5c0f47a452..f966cd251e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
@@ -30,6 +30,7 @@ import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
 import im.vector.app.core.epoxy.loadingItem
 import im.vector.app.core.epoxy.noResultItem
 import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.resources.UserPreferencesProvider
 import im.vector.app.core.ui.list.GenericHeaderItem_
 import im.vector.app.features.home.AvatarRenderer
 import org.matrix.android.sdk.api.session.Session
@@ -43,7 +44,8 @@ class SearchResultController @Inject constructor(
         private val session: Session,
         private val avatarRenderer: AvatarRenderer,
         private val stringProvider: StringProvider,
-        private val dateFormatter: VectorDateFormatter
+        private val dateFormatter: VectorDateFormatter,
+        private val userPreferencesProvider: UserPreferencesProvider
 ) : TypedEpoxyController() {
 
     var listener: Listener? = null
@@ -123,6 +125,7 @@ class SearchResultController @Inject constructor(
                     .sender(eventAndSender.sender
                             ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem())
                     .threadDetails(event.threadDetails)
+                    .areThreadMessagesEnabled(userPreferencesProvider.areThreadMessagesEnabled())
                     .listener { listener?.onItemClicked(eventAndSender.event) }
                     .let { result.add(it) }
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
index fcddc2286b..8e10ea94a6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
@@ -43,6 +43,7 @@ abstract class SearchResultItem : VectorEpoxyModel() {
     @EpoxyAttribute lateinit var spannable: EpoxyCharSequence
     @EpoxyAttribute var sender: MatrixItem? = null
     @EpoxyAttribute var threadDetails: ThreadDetails? = null
+    @EpoxyAttribute var areThreadMessagesEnabled: Boolean = false
 
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
 
@@ -55,7 +56,7 @@ abstract class SearchResultItem : VectorEpoxyModel() {
         holder.timeView.text = formattedDate
         holder.contentView.text = spannable.charSequence
 
-        if (BuildConfig.THREADING_ENABLED) {
+        if (areThreadMessagesEnabled) {
             threadDetails?.let {
                 if (it.isRootThread) {
                     showThreadSummary(holder)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index e979704d6a..4a7f1a5e03 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -447,7 +447,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     private fun canReplyInThread(event: TimelineEvent,
                                  messageContent: MessageContent?,
                                  actionPermissions: ActionPermissions): Boolean {
-        if (!BuildConfig.THREADING_ENABLED) return false
+        if (!vectorPreferences.areThreadMessagesEnabled()) return false
         if (initialState.isFromThreadTimeline) return false
         if (event.root.getClearType() != EventType.MESSAGE &&
                 !event.isSticker() && !event.isPoll()) return false
@@ -472,7 +472,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     private fun canViewInRoom(event: TimelineEvent,
                               messageContent: MessageContent?,
                               actionPermissions: ActionPermissions): Boolean {
-        if (!BuildConfig.THREADING_ENABLED) return false
+        if (!vectorPreferences.areThreadMessagesEnabled()) return false
         if (!initialState.isFromThreadTimeline) return false
         if (event.root.getClearType() != EventType.MESSAGE &&
                 !event.isSticker() && !event.isPoll()) return false
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 a2cdbec7c6..845b765101 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
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
 import im.vector.app.EmojiCompatFontProvider
 import im.vector.app.R
 import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.resources.UserPreferencesProvider
 import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@@ -31,6 +32,7 @@ class MessageItemAttributesFactory @Inject constructor(
         private val messageColorProvider: MessageColorProvider,
         private val avatarSizeProvider: AvatarSizeProvider,
         private val stringProvider: StringProvider,
+        private val preferencesProvider: UserPreferencesProvider,
         private val emojiCompatFontProvider: EmojiCompatFontProvider) {
 
     fun create(messageContent: Any?,
@@ -57,7 +59,8 @@ class MessageItemAttributesFactory @Inject constructor(
                 readReceiptsCallback = callback,
                 emojiTypeFace = emojiCompatFontProvider.typeface,
                 decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message),
-                threadDetails = threadDetails
+                threadDetails = threadDetails,
+                areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled()
         )
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 6d23d22ff0..29ecdd361e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -152,12 +152,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
             return true
         }
 
-        if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread()) {
+        if (userPreferencesProvider.areThreadMessagesEnabled() && !isFromThreadTimeline && root.isThread()) {
             return true
         }
 
-        if (BuildConfig.THREADING_ENABLED && isFromThreadTimeline) {
-            // //
+        if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) {
             return if (root.getRootThreadEventId() == rootThreadEventId) {
                 false
             } else root.eventId != rootThreadEventId
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 33eae89e31..484309571f 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
@@ -113,7 +113,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
         holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
 
         // Threads
-        if (BuildConfig.THREADING_ENABLED) {
+        if (attributes.areThreadMessagesEnabled) {
             holder.threadSummaryConstraintLayout.onClick(_threadClickListener)
             attributes.threadDetails?.let { threadDetails ->
                 holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
@@ -186,7 +186,8 @@ abstract class AbsMessageItem : AbsBaseMessageItem
             override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
             val emojiTypeFace: Typeface? = null,
             val decryptionErrorMessage: String? = null,
-            val threadDetails: ThreadDetails? = null
+            val threadDetails: ThreadDetails? = null,
+            val areThreadMessagesEnabled: 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/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index f46ab86c7c..148b7e3841 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -197,6 +197,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
         private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
 
         private const val SETTINGS_LABS_ENABLE_POLLS = "SETTINGS_LABS_ENABLE_POLLS"
+        const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES"
 
         // Possible values for TAKE_PHOTO_VIDEO_MODE
         const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
@@ -995,4 +996,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
     fun labsEnablePolls(): Boolean {
         return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_POLLS, false)
     }
+
+    fun areThreadMessagesEnabled(): Boolean {
+        return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, false)
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
index a83b4c33f4..3dd59e666d 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
@@ -16,8 +16,11 @@
 
 package im.vector.app.features.settings
 
+import androidx.preference.Preference
 import im.vector.app.R
 import im.vector.app.core.preference.VectorSwitchPreference
+import im.vector.app.features.MainActivity
+import im.vector.app.features.MainActivityArgs
 import javax.inject.Inject
 
 class VectorSettingsLabsFragment @Inject constructor(
@@ -32,5 +35,15 @@ class VectorSettingsLabsFragment @Inject constructor(
             // ensure correct default
             pref.isChecked = vectorPreferences.labsAutoReportUISI()
         }
+
+        // clear cache
+        findPreference(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let {
+            it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+                displayLoadingView()
+                MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true))
+                false
+            }
+        }
+
     }
 }
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index bad94dd158..c35e501d7c 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -3600,6 +3600,8 @@
 
     Auto Report Decryption Errors.
     Your system will automatically send logs when an unable to decrypt error occurs
+    Enable Thread Messages
+    Note: app will be restarted
 
     %s invites you
 
diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
index d2e9df4985..07601d8b8d 100644
--- a/vector/src/main/res/xml/vector_settings_labs.xml
+++ b/vector/src/main/res/xml/vector_settings_labs.xml
@@ -56,11 +56,16 @@
         android:key="SETTINGS_LABS_ENABLE_POLLS"
         android:title="@string/labs_enable_polls" />
 
+    
 
-        
+    
 
 
\ No newline at end of file

From 38f193fbd5947b780a30089e9cdefb6b7e4b43be Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Wed, 19 Jan 2022 18:52:02 +0200
Subject: [PATCH 101/130] Add LightweightSettingsStorage in sdk Enable thread
 awareness when threads are disabled Enhance enable/disable thread messages to
 app & sdk Add Shared PReferences to sdk

---
 matrix-sdk-android/build.gradle               |  3 ++
 .../java/org/matrix/android/sdk/api/Matrix.kt |  5 --
 .../room/model/relation/ReplyToContent.kt     |  3 +-
 .../lightweight/LightweightSettingsStorage.kt | 47 +++++++++++++++++++
 .../room/send/LocalEchoEventFactory.kt        |  4 +-
 .../internal/session/room/send/TextContent.kt |  3 +-
 .../session/room/timeline/DefaultTimeline.kt  |  3 ++
 .../room/timeline/DefaultTimelineService.kt   |  5 +-
 .../room/timeline/LoadTimelineStrategy.kt     |  3 ++
 .../session/room/timeline/TimelineChunk.kt    |  9 ++--
 .../room/timeline/TimelineEventDecryptor.kt   | 21 +++++----
 .../room/timeline/TokenChunkEventPersistor.kt | 10 ++--
 .../session/sync/SyncResponseHandler.kt       |  9 ++--
 .../sync/handler/room/RoomSyncHandler.kt      | 23 ++++-----
 .../handler/room/ThreadsAwarenessHandler.kt   |  4 ++
 .../app/core/platform/VectorBaseActivity.kt   |  1 -
 .../settings/VectorSettingsLabsFragment.kt    |  8 +++-
 17 files changed, 115 insertions(+), 46 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt

diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index b65c4718ba..efef826a87 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -141,6 +141,9 @@ dependencies {
 
     kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
 
+    // Shared Preferences
+    implementation libs.androidx.preferenceKtx
+
     // Work
     implementation libs.androidx.work
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index f903898398..901ba75d16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -96,11 +96,6 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
 
     companion object {
 
-        /**
-         * Determines whether or not thread messages are enabled
-         */
-        var areThreadMessagesEnabled: Boolean = false
-
         private lateinit var instance: Matrix
         private val isInit = AtomicBoolean(false)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
index 251328bea2..91bd5edc60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
@@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 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? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
new file mode 100644
index 0000000000..700b94a985
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.internal.database.lightweight
+
+import android.content.Context
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import javax.inject.Inject
+
+/**
+ * The purpose of this class is to provide an alternative and lightweight way to store settings/data
+ * on the sdi without using the database. This should be used just for sdk/user preferences and
+ * not for large data sets
+ */
+
+class LightweightSettingsStorage  @Inject constructor(context: Context) {
+
+    private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
+
+    fun setThreadMessagesEnabled(enabled: Boolean) {
+        sdkDefaultPrefs.edit {
+            putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
+        }
+    }
+
+    fun areThreadMessagesEnabled(): Boolean {
+        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
+    }
+
+    companion object {
+        const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
+    }
+}
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 53ea19e761..e9244b6793 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
@@ -471,8 +471,8 @@ internal class LocalEchoEventFactory @Inject constructor(
                 RelationDefaultContent(
                         type = RelationType.IO_THREAD,
                         eventId = it,
-                        inReplyTo = ReplyToContent(eventId))
-            } ?: RelationDefaultContent(null, null, ReplyToContent(eventId))
+                        inReplyTo = ReplyToContent(eventId = eventId))
+            } ?: RelationDefaultContent(null, null, ReplyToContent( eventId = eventId))
 
     private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
         return REPLY_PATTERN.format(
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 0bf0561599..a267f20c39 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
@@ -21,6 +21,7 @@ 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.RelationDefaultContent
+import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
 
@@ -48,7 +49,7 @@ fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String =
             msgType = msgType,
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
             body = text,
-            relatesTo = RelationDefaultContent(RelationType.IO_THREAD, rootThreadEventId),
+            relatesTo = RelationDefaultContent(type = RelationType.IO_THREAD, eventId = rootThreadEventId, inReplyTo = ReplyToContent(eventId = "CYIpEhDXkImqKD2TF9NSocxt4vU6hh98yXi5Ncusdaw")),
             formattedBody = formattedText
     )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index b2b033c0bb..3dd4225b2c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
@@ -60,6 +61,7 @@ internal class DefaultTimeline(private val roomId: String,
                                timelineEventMapper: TimelineEventMapper,
                                timelineInput: TimelineInput,
                                threadsAwarenessHandler: ThreadsAwarenessHandler,
+                               lightweightSettingsStorage: LightweightSettingsStorage,
                                eventDecryptor: TimelineEventDecryptor) : Timeline {
 
     companion object {
@@ -92,6 +94,7 @@ internal class DefaultTimeline(private val roomId: String,
             timelineInput = timelineInput,
             timelineEventMapper = timelineEventMapper,
             threadsAwarenessHandler = threadsAwarenessHandler,
+            lightweightSettingsStorage = lightweightSettingsStorage,
             onEventsUpdated = this::sendSignalToPostSnapshot,
             onLimitedTimeline = this::onLimitedTimeline,
             onNewTimelineEvents = this::onNewTimelineEvents
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index 278513fe55..200de10e86 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
@@ -37,6 +37,7 @@ import org.matrix.android.sdk.internal.database.RealmSessionProvider
 import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
 import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
 import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
@@ -64,6 +65,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val readReceiptHandler: ReadReceiptHandler,
         private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) : TimelineService {
@@ -88,7 +90,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 loadRoomMembersTask = loadRoomMembersTask,
                 readReceiptHandler = readReceiptHandler,
                 getEventTask = contextOfEventTask,
-                threadsAwarenessHandler = threadsAwarenessHandler
+                threadsAwarenessHandler = threadsAwarenessHandler,
+                lightweightSettingsStorage = lightweightSettingsStorage
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index efc11a8bde..f332c4a35f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
@@ -80,6 +81,7 @@ internal class LoadTimelineStrategy(
             val timelineInput: TimelineInput,
             val timelineEventMapper: TimelineEventMapper,
             val threadsAwarenessHandler: ThreadsAwarenessHandler,
+            val lightweightSettingsStorage: LightweightSettingsStorage,
             val onEventsUpdated: (Boolean) -> Unit,
             val onLimitedTimeline: () -> Unit,
             val onNewTimelineEvents: (List) -> Unit
@@ -241,6 +243,7 @@ internal class LoadTimelineStrategy(
                     timelineEventMapper = dependencies.timelineEventMapper,
                     uiEchoManager = uiEchoManager,
                     threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
+                    lightweightSettingsStorage = dependencies.lightweightSettingsStorage,
                     initialEventId = mode.originEventId(),
                     onBuiltEvents = dependencies.onEventsUpdated
             )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 9db9fe7c93..fdb985d584 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -23,13 +23,13 @@ import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.Sort
 import kotlinx.coroutines.CompletableDeferred
-import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -56,6 +56,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                              private val timelineEventMapper: TimelineEventMapper,
                              private val uiEchoManager: UIEchoManager? = null,
                              private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+                             private val lightweightSettingsStorage: LightweightSettingsStorage,
                              private val initialEventId: String?,
                              private val onBuiltEvents: (Boolean) -> Unit) {
 
@@ -296,7 +297,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
 
         if (timelineEvents.isEmpty()) return LoadedFromStorage()
 // Disabled due to the new fallback
-//        fetchRootThreadEventsIfNeeded(timelineEvents)
+        if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            fetchRootThreadEventsIfNeeded(timelineEvents)
+        }
         if (direction == Timeline.Direction.FORWARDS) {
             builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
         }
@@ -333,7 +336,6 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
      * in order to be able to display the event to the user appropriately
      */
     private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List) {
-//        if (BuildConfig.THREADING_ENABLED) return
         val eventEntityList = offsetResults
                 .mapNotNull {
                     it.root
@@ -486,6 +488,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineEventMapper = timelineEventMapper,
                 uiEchoManager = uiEchoManager,
                 threadsAwarenessHandler = threadsAwarenessHandler,
+                lightweightSettingsStorage = lightweightSettingsStorage,
                 initialEventId = null,
                 onBuiltEvents = this.onBuiltEvents
         )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index 5913aa6e7f..0e719c7b1d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.Realm
 import io.realm.RealmConfiguration
-import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 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.toModel
 import org.matrix.android.sdk.internal.crypto.NewSessionListener
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
@@ -37,7 +37,8 @@ internal class TimelineEventDecryptor @Inject constructor(
         @SessionDatabase
         private val realmConfiguration: RealmConfiguration,
         private val cryptoService: CryptoService,
-        private val threadsAwarenessHandler: ThreadsAwarenessHandler
+        private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+        private val lightweightSettingsStorage: LightweightSettingsStorage
 ) {
 
     private val newSessionListener = object : NewSessionListener {
@@ -117,15 +118,15 @@ internal class TimelineEventDecryptor @Inject constructor(
                 eventEntity?.apply {
                     val decryptedPayload =
 // Disabled due to the new fallback
-//                            if (!BuildConfig.THREADING_ENABLED) {
-//                                threadsAwarenessHandler.handleIfNeededDuringDecryption(
-//                                        it,
-//                                        roomId = event.roomId,
-//                                        event,
-//                                        result)
-//                            } else {
+                            if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+                                threadsAwarenessHandler.handleIfNeededDuringDecryption(
+                                        it,
+                                        roomId = event.roomId,
+                                        event,
+                                        result)
+                            } else {
                                 null
-//                            }
+                            }
                     setDecryptionResult(result, decryptedPayload)
                 }
             }
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 bf7ebbec54..6f49363d3e 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
@@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.room.timeline
 import com.zhuinden.monarchy.Monarchy
 import dagger.Lazy
 import io.realm.Realm
-import org.matrix.android.sdk.api.Matrix
-import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@@ -29,6 +27,7 @@ import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addStateEvent
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
 import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 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
@@ -52,7 +51,7 @@ import javax.inject.Inject
 internal class TokenChunkEventPersistor @Inject constructor(
                                                             @SessionDatabase private val monarchy: Monarchy,
                                                             @UserId private val userId: String,
-                                                            private val matrixConfiguration: MatrixConfiguration,
+                                                            private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                             private val liveEventManager: Lazy) {
 
     enum class Result {
@@ -185,7 +184,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
                 currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
-                if(Matrix.areThreadMessagesEnabled) {
+                if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
                     eventEntity.rootThreadEventId?.let {
                         // This is a thread event
                         optimizedThreadSummaryMap[it] = eventEntity
@@ -199,7 +198,8 @@ internal class TokenChunkEventPersistor @Inject constructor(
         if (currentChunk.isValid) {
             RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
         }
-        if(Matrix.areThreadMessagesEnabled) {
+
+        if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
             optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index cfe021f08c..5e9a347818 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.sync
 
 import androidx.work.ExistingPeriodicWorkPolicy
 import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.pushrules.PushRuleService
 import org.matrix.android.sdk.api.pushrules.RuleScope
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
@@ -27,6 +26,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -65,6 +65,7 @@ internal class SyncResponseHandler @Inject constructor(
         private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
         private val cryptoService: DefaultCryptoService,
         private val tokenStore: SyncTokenStore,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val processEventForPushTask: ProcessEventForPushTask,
         private val pushRuleService: PushRuleService,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -103,9 +104,9 @@ internal class SyncResponseHandler @Inject constructor(
 
         // Prerequisite for thread events handling in RoomSyncHandler
 // Disabled due to the new fallback
-//        if (!BuildConfig.THREADING_ENABLED) {
-//            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
-//        }
+        if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+        }
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
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 cf0cdc1015..e8c06e2aeb 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
@@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 import dagger.Lazy
 import io.realm.Realm
 import io.realm.kotlin.createObject
-import org.matrix.android.sdk.api.Matrix
-import org.matrix.android.sdk.api.MatrixConfiguration
 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
@@ -39,6 +37,7 @@ 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.helper.updateThreadSummaryIfNeeded
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 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
@@ -83,9 +82,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomMemberEventHandler: RoomMemberEventHandler,
                                                    private val roomTypingUsersHandler: RoomTypingUsersHandler,
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
-                                                   private val matrixConfiguration: MatrixConfiguration,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
+                                                   private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                    private val timelineInput: TimelineInput,
                                                    private val liveEventService: Lazy) {
 
@@ -373,6 +372,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             if (event.eventId == null || event.senderId == null || event.type == null) {
                 continue
             }
+
             eventIds.add(event.eventId)
             liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC)
 
@@ -382,12 +382,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 decryptIfNeeded(event, roomId)
             }
 // Disabled due to the new fallback
-//            if (!BuildConfig.THREADING_ENABLED) {
-//                threadsAwarenessHandler.handleIfNeeded(
-//                        realm = realm,
-//                        roomId = roomId,
-//                        event = event)
-//            }
+            if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+                threadsAwarenessHandler.handleIfNeeded(
+                        realm = realm,
+                        roomId = roomId,
+                        event = event)
+            }
 
             val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
@@ -410,7 +410,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
 
             chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
-            if(Matrix.areThreadMessagesEnabled) {
+            if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
                 eventEntity.rootThreadEventId?.let {
                     // This is a thread event
                     optimizedThreadSummaryMap[it] = eventEntity
@@ -446,7 +446,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
         // Handle deletion of [stuck] local echos if needed
         deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
-        if(Matrix.areThreadMessagesEnabled) {
+
+        if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
             optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
                     roomId = roomId,
                     realm = realm,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index d093aab21f..d06c59e657 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
 import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -280,6 +281,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      */
     private fun getRootThreadEventId(event: Event): String? =
             event.content.toModel()?.relatesTo?.eventId
+//    private fun getRootThreadEventId(event: Event): String? =
+//            event.content.toModel()?.relatesTo?.inReplyTo?.eventId ?:
+//            event.content.toModel()?.relatesTo?.eventId
 
     @Suppress("UNCHECKED_CAST")
     private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index 67463a2ead..137d3780e8 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -194,7 +194,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
         navigator = singletonEntryPoint.navigator()
         activeSessionHolder = singletonEntryPoint.activeSessionHolder()
         vectorPreferences = singletonEntryPoint.vectorPreferences()
-        Matrix.areThreadMessagesEnabled = vectorPreferences.areThreadMessagesEnabled()
         configurationViewModel.activityRestarter.observe(this) {
             if (!it.hasBeenHandled) {
                 // Recreate the Activity because configuration has changed
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
index 3dd59e666d..895c6fcd74 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
@@ -21,10 +21,13 @@ import im.vector.app.R
 import im.vector.app.core.preference.VectorSwitchPreference
 import im.vector.app.features.MainActivity
 import im.vector.app.features.MainActivityArgs
+import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import javax.inject.Inject
 
 class VectorSettingsLabsFragment @Inject constructor(
-        private val vectorPreferences: VectorPreferences
+        private val vectorPreferences: VectorPreferences,
+        private val lightweightSettingsStorage: LightweightSettingsStorage
+
 ) : VectorSettingsBaseFragment() {
 
     override var titleRes = R.string.room_settings_labs_pref_title
@@ -38,7 +41,8 @@ class VectorSettingsLabsFragment @Inject constructor(
 
         // clear cache
         findPreference(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let {
-            it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+            it.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref->
+                lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
                 displayLoadingView()
                 MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true))
                 false

From 35ee72aac0fb584b48a7fc9c77b37605115de60c Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 20 Jan 2022 00:50:44 +0200
Subject: [PATCH 102/130] Add typealias for TimelineEvent

---
 .../src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt   | 6 ++++--
 .../app/features/home/room/detail/TimelineViewModel.kt      | 4 ++--
 .../home/room/threads/list/viewmodel/ThreadListViewModel.kt | 6 +++---
 .../app/features/settings/VectorSettingsLabsFragment.kt     | 2 +-
 4 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index 46acdc123b..1e239069ad 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -32,6 +32,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 
+typealias ThreadRootEvent = TimelineEvent
+
 class FlowRoom(private val room: Room) {
 
     fun liveRoomSummary(): Flow> {
@@ -99,14 +101,14 @@ class FlowRoom(private val room: Room) {
         return room.getLiveRoomNotificationState().asFlow()
     }
 
-    fun liveThreadList(): Flow> {
+    fun liveThreadList(): Flow> {
         return room.getAllThreadsLive().asFlow()
                 .startWith(room.coroutineDispatchers.io) {
                     room.getAllThreads()
                 }
     }
 
-    fun liveLocalUnreadThreadList(): Flow> {
+    fun liveLocalUnreadThreadList(): Flow> {
         return room.getNumberOfLocalThreadNotificationsLive().asFlow()
                 .startWith(room.coroutineDispatchers.io) {
                     room.getNumberOfLocalThreadNotifications()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index d051860f32..563ab00d18 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -326,8 +326,8 @@ class TimelineViewModel @AssistedInject constructor(
                 .liveLocalUnreadThreadList()
                 .execute {
                     val threadList = it.invoke()
-                    val isUserMentioned = threadList?.firstOrNull { timelineEvent ->
-                        timelineEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
+                    val isUserMentioned = threadList?.firstOrNull { threadRootEvent ->
+                        threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
                     }?.let { true } ?: false
                     val numberOfLocalUnreadThreads = threadList?.size ?: 0
                     copy(threadNotificationBadgeState = ThreadNotificationBadgeState(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
index 25dc14cec6..88751004a4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt
@@ -62,9 +62,9 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
         room?.flow()
                 ?.liveThreadList()
                 ?.map {
-                    it.map { timelineEvent ->
-                        val isParticipating = room.isUserParticipatingInThread(timelineEvent.eventId)
-                        ThreadTimelineEvent(timelineEvent, isParticipating)
+                    it.map { threadRootEvent ->
+                        val isParticipating = room.isUserParticipatingInThread(threadRootEvent.eventId)
+                        ThreadTimelineEvent(threadRootEvent, isParticipating)
                     }
                 }
                 ?.flowOn(room.coroutineDispatchers.io)
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
index 895c6fcd74..7a3e0033e3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt
@@ -41,7 +41,7 @@ class VectorSettingsLabsFragment @Inject constructor(
 
         // clear cache
         findPreference(VectorPreferences.SETTINGS_LABS_ENABLE_THREAD_MESSAGES)?.let {
-            it.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref->
+            it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                 lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
                 displayLoadingView()
                 MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true))

From e0630ceac05c6c427f010c1c6651a25c940e2170 Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Thu, 20 Jan 2022 13:02:35 +0200
Subject: [PATCH 103/130] Fix mentions UI within threads

---
 .../internal/session/room/relation/DefaultRelationService.kt  | 2 +-
 .../sdk/internal/session/room/send/LocalEchoEventFactory.kt   | 4 ++--
 .../home/room/detail/composer/MessageComposerViewModel.kt     | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

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 eee553ab80..08154b9cbd 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
@@ -192,7 +192,7 @@ internal class DefaultRelationService @AssistedInject constructor(
             eventFactory.createThreadTextEvent(
                     rootThreadEventId = rootThreadEventId,
                     roomId = roomId,
-                    text = replyInThreadText.toString(),
+                    text = replyInThreadText,
                     msgType = msgType,
                     autoMarkdown = autoMarkdown,
                     formattedText = formattedText)
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 e9244b6793..7a4edf582f 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
@@ -396,11 +396,11 @@ internal class LocalEchoEventFactory @Inject constructor(
     fun createThreadTextEvent(
             rootThreadEventId: String,
             roomId: String,
-            text: String,
+            text: CharSequence,
             msgType: String,
             autoMarkdown: Boolean,
             formattedText: String?): Event {
-        val content = formattedText?.let { TextContent(text, it) } ?: createTextContent(text, autoMarkdown)
+        val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
         return createEvent(
                 roomId,
                 EventType.MESSAGE,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index 00755a78e7..b7425af7c9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -191,7 +191,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             if (state.rootThreadEventId != null) {
                                 room.replyInThread(
                                         rootThreadEventId = state.rootThreadEventId,
-                                        replyInThreadText = action.text.toString(),
+                                        replyInThreadText = action.text,
                                         autoMarkdown = action.autoMarkdown)
                             } else {
                                 room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)

From fe88e81d4aa8196c4bfd57070c846e204e29642f Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos 
Date: Mon, 24 Jan 2022 16:55:15 +0200
Subject: [PATCH 104/130] - Refactor thread awareness (handle decrypted rooms,
 images, stickers etc) - Enable/disable threads functionality - New fallback
 thread implementation

---
 .../sdk/api/session/events/model/Event.kt     |   9 +-
 .../database/helper/ThreadEventsHelper.kt     |   4 +-
 .../internal/database/mapper/EventMapper.kt   |   8 +-
 .../internal/database/model/EventEntity.kt    |   5 +-
 .../threads/FetchThreadTimelineTask.kt        |   5 -
 .../room/send/LocalEchoEventFactory.kt        |  72 ++++-
 .../session/room/send/LocalEchoRepository.kt  |  11 +-
 .../internal/session/room/send/TextContent.kt |  17 +-
 .../session/room/timeline/TimelineChunk.kt    |  11 +-
 .../room/timeline/TimelineEventDecryptor.kt   |  37 ++-
 .../room/timeline/TokenChunkEventPersistor.kt |   4 +-
 .../session/sync/SyncResponseHandler.kt       |   6 +-
 .../sync/handler/room/RoomSyncHandler.kt      |  15 +-
 .../handler/room/ThreadsAwarenessHandler.kt   | 277 +++++++++++-------
 .../app/core/platform/VectorBaseActivity.kt   |   1 -
 .../command/AutocompleteCommandPresenter.kt   |   1 -
 .../app/features/command/CommandParser.kt     |   1 -
 .../home/room/detail/TimelineFragment.kt      |   1 -
 .../home/room/detail/TimelineViewModel.kt     |   2 +
 .../room/detail/search/SearchResultItem.kt    |   1 -
 .../action/MessageActionsViewModel.kt         |   1 -
 .../helper/TimelineEventVisibilityHelper.kt   |   1 -
 .../detail/timeline/item/AbsMessageItem.kt    |   1 -
 .../settings/VectorSettingsLabsFragment.kt    |   1 -
 24 files changed, 325 insertions(+), 167 deletions(-)

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 047aefe88d..166c24d19a 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
@@ -201,18 +201,19 @@ data class Event(
     fun getDecryptedTextSummary(): String? {
         val text = getDecryptedValue() ?: return null
         return when {
-            isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
+            isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage()        -> "sent a file."
             isAudioMessage()       -> "sent an audio file."
             isImageMessage()       -> "sent an image."
             isVideoMessage()       -> "sent a video."
+            isSticker()            -> "sent a sticker"
             isPoll()               -> getPollQuestion() ?: "created a poll."
             else                   -> text
         }
     }
 
     private fun Event.isQuote(): Boolean {
-        if (isReply()) return false
+        if (isReplyRenderedInThread()) return false
         return getDecryptedValue("formatted_body")?.contains("
") ?: false } @@ -374,6 +375,10 @@ fun Event.isReply(): Boolean { return getRelationContent()?.inReplyTo?.eventId != null } +fun Event.isReplyRenderedInThread(): Boolean { + return isReply() && getRelationContent()?.inReplyTo?.renderIn?.contains("m.thread") == true +} + fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 71be1f3a4a..33829712c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -20,7 +20,6 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.ChunkEntity @@ -40,7 +39,6 @@ internal fun Map.updateThreadSummaryIfNeeded( roomId: String, realm: Realm, currentUserId: String, shouldUpdateNotifications: Boolean = true) { - for ((rootThreadEventId, eventEntity) in this) { eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let { if (it.isNullOrEmpty()) return@let @@ -57,7 +55,7 @@ internal fun Map.updateThreadSummaryIfNeeded( } } - if(shouldUpdateNotifications) { + if (shouldUpdateNotifications) { updateNotificationsNew(roomId, realm, currentUserId) } } 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 3504284427..c10b9084ea 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 @@ -125,9 +125,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event { return EventMapper.map(this, castJsonNumbers) } -internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { +internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity { return EventMapper.map(this, roomId).apply { this.sendState = sendState this.ageLocalTs = ageLocalTs + contentToInject?.let { + this.content = it + if (this.type == EventType.STICKER) { + this.type = EventType.MESSAGE + } + } } } 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 30b3abbd9e..445181e576 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 @@ -20,7 +20,6 @@ import io.realm.RealmObject import io.realm.annotations.Index import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState -import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider @@ -80,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "", companion object - fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { + fun setDecryptionResult(result: MXEventDecryptionResult) { assertIsManaged() val decryptionResult = OlmDecryptionResult( - payload = clearEvent ?: result.clearEvent, + payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index f50a691d43..e0d501c515 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -83,11 +83,9 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( val threadList = response.chunks + listOfNotNull(response.originalEvent) - return storeNewEventsIfNeeded(threadList, params.roomId) } - /** * Store new events if they are not already received, and returns weather or not, * a timeline update should be made @@ -105,7 +103,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( val roomMemberContentsByUser = HashMap() for (event in threadList.reversed()) { - if (event.eventId == null || event.senderId == null || event.type == null) { eventsSkipped++ continue @@ -180,7 +177,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( private fun handleReaction(realm: Realm, event: Event, roomId: String) { - val unsignedData = event.unsignedData ?: return val relatedEventId = event.eventId ?: return @@ -206,7 +202,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( sum.count += 1 } } - } } } 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 7a4edf582f..29ab2c7f0b 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 @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.UnsignedData 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.message.AudioInfo import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo import org.matrix.android.sdk.api.session.room.model.message.FileInfo @@ -41,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent 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.MessageVideoContent @@ -293,7 +295,13 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) } + relatesTo = rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.IO_THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) + ) + } ) return createMessageEvent(roomId, content) } @@ -330,7 +338,13 @@ internal class LocalEchoEventFactory @Inject constructor( thumbnailInfo = thumbnailInfo ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) } + relatesTo = rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.IO_THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) + ) + } ) return createMessageEvent(roomId, content) } @@ -354,7 +368,13 @@ internal class LocalEchoEventFactory @Inject constructor( waveform = waveformSanitizer.sanitize(attachment.waveform) ), voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(), - relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) } + relatesTo = rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.IO_THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) + ) + } ) return createMessageEvent(roomId, content) } @@ -368,7 +388,13 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) } + relatesTo = rootThreadEventId?.let { + RelationDefaultContent( + type = RelationType.IO_THREAD, + eventId = it, + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) + ) + } ) return createMessageEvent(roomId, content) } @@ -378,6 +404,7 @@ internal class LocalEchoEventFactory @Inject constructor( } fun createEvent(roomId: String, type: String, content: Content?): Event { + val newContent = enhanceStickerIfNeeded(type, content) ?: content val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -385,11 +412,31 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = type, - content = content, + content = newContent, unsignedData = UnsignedData(age = null, transactionId = localId) ) } + /** + * Enhance sticker to support threads fallback if needed + */ + private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? { + var newContent: Content? = null + if (type == EventType.STICKER) { + val isThread = (content.toModel())?.relatesTo?.type == RelationType.IO_THREAD + val rootThreadEventId = (content.toModel())?.relatesTo?.eventId + if (isThread && rootThreadEventId != null) { + val newRelationalDefaultContent = (content.toModel())?.relatesTo?.copy( + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId)) + ) + newContent = (content.toModel())?.copy( + relatesTo = newRelationalDefaultContent + ).toContent() + } + } + return newContent + } + /** * Creates a thread event related to the already existing root event */ @@ -404,7 +451,10 @@ internal class LocalEchoEventFactory @Inject constructor( return createEvent( roomId, EventType.MESSAGE, - content.toThreadTextContent(rootThreadEventId, msgType) + content.toThreadTextContent( + rootThreadEventId = rootThreadEventId, + latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), + msgType = msgType) .toContent()) } @@ -471,8 +521,8 @@ internal class LocalEchoEventFactory @Inject constructor( RelationDefaultContent( type = RelationType.IO_THREAD, eventId = it, - inReplyTo = ReplyToContent(eventId = eventId)) - } ?: RelationDefaultContent(null, null, ReplyToContent( eventId = eventId)) + inReplyTo = ReplyToContent(eventId = eventId, renderIn = arrayListOf("m.thread"))) + } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId)) private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String { return REPLY_PATTERN.format( @@ -584,7 +634,10 @@ internal class LocalEchoEventFactory @Inject constructor( roomId, markdownParser .parse(quoteText, force = true, advanced = autoMarkdown) - .toThreadTextContent(rootThreadEventId, MessageType.MSGTYPE_TEXT) + .toThreadTextContent( + rootThreadEventId = rootThreadEventId, + latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), + msgType = MessageType.MSGTYPE_TEXT) ) } else { createFormattedTextEvent( @@ -625,6 +678,7 @@ internal class LocalEchoEventFactory @Inject constructor( // // No whitespace because currently breaks temporary formatted text to Span const val REPLY_PATTERN = """
In reply to %s
%s
%s""" + const val QUOTE_PATTERN = """

%s

%s

""" // This is used to replace inner mx-reply tags val MX_REPLY_REGEX = ".*".toRegex() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 13095fbd58..1b1a66a1c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } - fun deleteFailedEchoAsync(roomId: String, eventId: String?) { + fun deleteFailedEchoAsync(roomId: String, eventId: String?) { monarchy.runTransactionSync { realm -> TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm() EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm() @@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } } + + /** + * Returns the latest known thread event message, or the rootThreadEventId if no other event found + */ + fun getLatestThreadEvent(rootThreadEventId: String): String { + return realmSessionProvider.withRealm { realm -> + EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId + } ?: rootThreadEventId + } } 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 a267f20c39..5c629f87f0 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 @@ -44,12 +44,25 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT) ) } -fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { +/** + * Transform a TextContent to a thread message content. It will also add the inReplyTo + * latestThreadEventId in order for the clients without threads enabled to render it appropriately + * If latest event not found, we pass rootThreadEventId + */ +fun TextContent.toThreadTextContent( + rootThreadEventId: String, + latestThreadEventId: String, + msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent { return MessageTextContent( msgType = msgType, format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null }, body = text, - relatesTo = RelationDefaultContent(type = RelationType.IO_THREAD, eventId = rootThreadEventId, inReplyTo = ReplyToContent(eventId = "CYIpEhDXkImqKD2TF9NSocxt4vU6hh98yXi5Ncusdaw")), + relatesTo = RelationDefaultContent( + type = RelationType.IO_THREAD, + eventId = rootThreadEventId, + inReplyTo = ReplyToContent( + eventId = latestThreadEventId + )), formattedBody = formattedText ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index fdb985d584..385551ea94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.Collections @@ -297,9 +298,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, if (timelineEvents.isEmpty()) return LoadedFromStorage() // Disabled due to the new fallback - if(!lightweightSettingsStorage.areThreadMessagesEnabled()) { - fetchRootThreadEventsIfNeeded(timelineEvents) - } +// if(!lightweightSettingsStorage.areThreadMessagesEnabled()) { +// fetchRootThreadEventsIfNeeded(timelineEvents) +// } if (direction == Timeline.Direction.FORWARDS) { builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) } } @@ -353,6 +354,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } } + if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { + // Thread aware for not encrypted events + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } return timelineEvent } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 0e719c7b1d..49a8a8b55a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.NewSessionListener import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase @@ -103,9 +104,27 @@ internal class TimelineEventDecryptor @Inject constructor( } } + private fun threadAwareNonEncryptedEvents(request: DecryptionRequest, realm: Realm) { + val event = request.event + realm.executeTransaction { + val eventId = event.eventId ?: return@executeTransaction + val eventEntity = EventEntity + .where(it, eventId = eventId) + .findFirst() + val decryptedEvent = eventEntity?.asDomain() + threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity) + } + } private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) { val event = request.event val timelineId = request.timelineId + + if (!request.event.isEncrypted()) { + // Here we have requested a decryption to an event that is not encrypted + // We will simply make this event thread aware + threadAwareNonEncryptedEvents(request, realm) + return + } try { val result = cryptoService.decryptEvent(request.event, timelineId) Timber.v("Successfully decrypted event ${event.eventId}") @@ -114,21 +133,9 @@ internal class TimelineEventDecryptor @Inject constructor( val eventEntity = EventEntity .where(it, eventId = eventId) .findFirst() - - eventEntity?.apply { - val decryptedPayload = -// Disabled due to the new fallback - if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { - threadsAwarenessHandler.handleIfNeededDuringDecryption( - it, - roomId = event.roomId, - event, - result) - } else { - null - } - setDecryptionResult(result, decryptedPayload) - } + eventEntity?.setDecryptionResult(result) + val decryptedEvent = eventEntity?.asDomain() + threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity) } } catch (e: MXCryptoError) { Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") 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 6f49363d3e..387085724c 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 @@ -184,7 +184,7 @@ internal class TokenChunkEventPersistor @Inject constructor( } liveEventManager.get().dispatchPaginatedEventReceived(event, roomId) currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) - if(lightweightSettingsStorage.areThreadMessagesEnabled()) { + if (lightweightSettingsStorage.areThreadMessagesEnabled()) { eventEntity.rootThreadEventId?.let { // This is a thread event optimizedThreadSummaryMap[it] = eventEntity @@ -199,7 +199,7 @@ internal class TokenChunkEventPersistor @Inject constructor( RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } - if(lightweightSettingsStorage.areThreadMessagesEnabled()) { + if (lightweightSettingsStorage.areThreadMessagesEnabled()) { optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 5e9a347818..f93da9705d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -104,9 +104,9 @@ internal class SyncResponseHandler @Inject constructor( // Prerequisite for thread events handling in RoomSyncHandler // Disabled due to the new fallback - if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { - threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) - } +// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { +// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) +// } // Start one big transaction monarchy.awaitTransaction { realm -> 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 e8c06e2aeb..f4f8cc01c6 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 @@ -381,16 +381,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle if (event.isEncrypted() && !isInitialSync) { decryptIfNeeded(event, roomId) } -// Disabled due to the new fallback - if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { - threadsAwarenessHandler.handleIfNeeded( - realm = realm, - roomId = roomId, - event = event) + var contentToInject: String? = null + if (!isInitialSync) { + contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event) } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId @@ -410,7 +407,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) - if(lightweightSettingsStorage.areThreadMessagesEnabled()) { + if (lightweightSettingsStorage.areThreadMessagesEnabled()) { eventEntity.rootThreadEventId?.let { // This is a thread event optimizedThreadSummaryMap[it] = eventEntity @@ -447,7 +444,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle // Handle deletion of [stuck] local echos if needed deleteLocalEchosIfNeeded(insertType, roomEntity, eventList) - if(lightweightSettingsStorage.areThreadMessagesEnabled()) { + if (lightweightSettingsStorage.areThreadMessagesEnabled()) { optimizedThreadSummaryMap.updateThreadSummaryIfNeeded( roomId = roomId, realm = realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index d06c59e657..67aade1d0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -18,11 +18,14 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import com.zhuinden.monarchy.Monarchy import io.realm.Realm -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.events.model.Content 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.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContentForType +import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId +import org.matrix.android.sdk.api.session.events.model.isSticker 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.message.MessageFormat @@ -32,20 +35,23 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType 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 -import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage +import org.matrix.android.sdk.internal.database.mapper.ContentMapper 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.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.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import org.matrix.android.sdk.internal.util.awaitTransaction -import timber.log.Timber import javax.inject.Inject /** @@ -55,11 +61,16 @@ import javax.inject.Inject */ internal class ThreadsAwarenessHandler @Inject constructor( private val permalinkFactory: PermalinkFactory, - private val cryptoService: CryptoService, @SessionDatabase private val monarchy: Monarchy, + private val lightweightSettingsStorage: LightweightSettingsStorage, private val getEventTask: GetEventTask ) { + // This caching is responsible to improve the performance when we receive a root event + // to be able to know this event is a root one without checking the DB, + // We update the list with all thread root events by checking if there is a m.thread relation on the events + private val cacheEventRootId = hashSetOf() + /** * Fetch root thread events if they are missing from the local storage * @param syncResponse the sync response @@ -142,120 +153,186 @@ internal class ThreadsAwarenessHandler @Inject constructor( /** * Handle events mainly coming from the RoomSyncHandler + * @return The content to inject in the roomSyncHandler live events */ - fun handleIfNeeded(realm: Realm, - roomId: String, - event: Event) { - val payload = transformThreadToReplyIfNeeded( - realm = realm, - roomId = roomId, - event = event, - decryptedResult = event.mxDecryptionResult?.payload) ?: return - - event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload) - } - - /** - * Handle events while they are being decrypted - */ - fun handleIfNeededDuringDecryption(realm: Realm, - roomId: String?, - event: Event, - result: MXEventDecryptionResult): JsonDict? { - return transformThreadToReplyIfNeeded( - realm = realm, - roomId = roomId, - event = event, - decryptedResult = result.clearEvent) - } - - /** - * If the event is a thread event then transform/enhance it to a visual Reply Event, - * If the event is not a thread event, null value will be returned - * If there is an error (ex. the root/origin thread event is not found), null will be returned - */ - private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? { + fun makeEventThreadAware(realm: Realm, + roomId: String?, + event: Event?, + eventEntity: EventEntity? = null): String? { + event ?: return null roomId ?: return null + if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null + handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event) if (!isThreadEvent(event)) return null - val rootThreadEventId = getRootThreadEventId(event) ?: return null - val payload = decryptedResult?.toMutableMap() ?: return null - var body = getValueFromPayload(payload, "body") ?: return null - val msgType = getValueFromPayload(payload, "msgtype") ?: run { - if (payload["type"]?.toString() == EventType.STICKER) { - MessageType.MSGTYPE_STICKER_LOCAL - } else { - return null + val eventPayload = if (!event.isEncrypted()) { + event.content?.toMutableMap() ?: return null + } else { + event.mxDecryptionResult?.payload?.toMutableMap() ?: return null + } + val eventBody = event.getDecryptedTextSummary() ?: return null + val eventIdToInject = getPreviousEventOrRoot(event) ?: run { + return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) + } + val eventToInject = getEventFromDB(realm, eventIdToInject) + val eventToInjectBody = eventToInject?.getDecryptedTextSummary() + var contentForNonEncrypted: String? + if (eventToInject != null && eventToInjectBody != null) { + // If the event to inject exists and is decrypted + // Inject it to our event + val messageTextContent = injectEvent( + roomId = roomId, + eventBody = eventBody, + eventToInject = eventToInject, + eventToInjectBody = eventToInjectBody) ?: return null + // update the event + contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent) + } else { + contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload) + } + + // 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) + } + return contentForNonEncrypted + } + + /** + * Handle for not thread events that we have marked them as root. + * Find relations and inject them accordingly + * @param eventEntity the current eventEntity received + * @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? { + if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) { + eventEntity?.let { + val eventBody = event.getDecryptedTextSummary() ?: return null + return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true) } } - val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null - val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null + return null + } - // Check the event type - when (msgType) { - MessageType.MSGTYPE_STICKER_LOCAL -> { - body = "sent a sticker from within a thread" - } - MessageType.MSGTYPE_FILE -> { - body = "sent a file from within a thread" - } - MessageType.MSGTYPE_VIDEO -> { - body = "Sent a video from within a thread" - } - MessageType.MSGTYPE_IMAGE -> { - body = "sent an image from within a thread" - } - MessageType.MSGTYPE_AUDIO -> { - body = "sent an audio file from within a thread" - } + /** + * This function is responsible to check if there is any event that relates to our current event + * This is useful when we receive an event that relates to a missing parent, so when later we receive the parent + * we can update the child as well + * @param event the current event that we examine + * @param eventBody the current body of the 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 + */ + private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? { + event.eventId ?: return null + val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null + eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound -> + val newEventFound = eventEntityFound.asDomain() + val newEventBody = newEventFound.getDecryptedTextSummary() ?: return null + val newEventPayload = newEventFound.mxDecryptionResult?.payload?.toMutableMap() ?: return null + + val messageTextContent = injectEvent( + roomId = roomId, + eventBody = newEventBody, + eventToInject = event, + eventToInjectBody = eventBody) ?: return null + + return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent) } - decryptIfNeeded(rootThreadEvent, roomId) + return null + } - val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body") + /** + * Actual update the eventEntity with the new payload + * @return the content to inject when this is executed by RoomSyncHandler + */ + private fun updateEventEntity(event: Event, + eventEntity: EventEntity?, + eventPayload: MutableMap, + messageTextContent: Content): String? { + eventPayload["content"] = messageTextContent - val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false) - val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" + if (event.isEncrypted()) { + if (event.isSticker()) { + eventPayload["type"] = EventType.MESSAGE + } + event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = eventPayload) + eventEntity?.decryptionResultJson = event.mxDecryptionResult?.let { + MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it) + } + } else { + if (event.type == EventType.STICKER) { + eventEntity?.type = EventType.MESSAGE + } + eventEntity?.content = ContentMapper.map(messageTextContent) + return ContentMapper.map(messageTextContent) + } + return null + } + /** + * Injecting $eventToInject decrypted content as a reply to $event + * @param eventToInject the event that will inject + * @param eventBody the actual event body + * @return The final content with the injected event + */ + private fun injectEvent(roomId: String, + eventBody: String, + eventToInject: Event, + eventToInjectBody: String): Content? { + val eventToInjectId = eventToInject.eventId ?: return null + val eventIdToInjectSenderId = eventToInject.senderId.orEmpty() + val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false) + val userLink = permalinkFactory.createPermalink(eventIdToInjectSenderId, false) ?: "" val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format( permalink, userLink, - rootThreadEventSenderId, - // Remove inner mx_reply tags if any - rootThreadEventBody, - body) + eventIdToInjectSenderId, + eventToInjectBody, + eventBody) + + return MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = eventBody, + formattedBody = replyFormatted + ).toContent() + } + + /** + * Integrate fallback Quote reply + */ + private fun injectFallbackIndicator(event: Event, + eventBody: String, + eventEntity: EventEntity?, + eventPayload: MutableMap): String? { + val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format( + "Replied within a thread", + eventBody) val messageTextContent = MessageTextContent( msgType = MessageType.MSGTYPE_TEXT, format = MessageFormat.FORMAT_MATRIX_HTML, - body = body, + body = eventBody, formattedBody = replyFormatted ).toContent() - payload["content"] = messageTextContent - - return payload + return updateEventEntity(event, eventEntity, eventPayload, messageTextContent) } - /** - * Decrypt the event - */ - - private fun decryptIfNeeded(event: Event, roomId: String) { - try { - if (!event.isEncrypted() || event.mxDecryptionResult != null) return - - // Event from sync does not have roomId, so add it to the event first - val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - if (e is MXCryptoError.Base) { - event.mCryptoError = e.errorType - event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription - } + private fun eventThatRelatesTo(realm: Realm, currentEventId: String, rootThreadEventId: String): List? { + val threadList = realm.where() + .beginGroup() + .equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .or() + .equalTo(EventEntityFields.EVENT_ID, rootThreadEventId) + .endGroup() + .and() + .findAll() + cacheEventRootId.add(rootThreadEventId) + return threadList.filter { + it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId } } @@ -281,9 +358,9 @@ internal class ThreadsAwarenessHandler @Inject constructor( */ private fun getRootThreadEventId(event: Event): String? = event.content.toModel()?.relatesTo?.eventId -// private fun getRootThreadEventId(event: Event): String? = -// event.content.toModel()?.relatesTo?.inReplyTo?.eventId ?: -// event.content.toModel()?.relatesTo?.eventId + + private fun getPreviousEventOrRoot(event: Event): String? = + event.content.toModel()?.relatesTo?.inReplyTo?.eventId @Suppress("UNCHECKED_CAST") private fun getValueFromPayload(payload: JsonDict?, key: String): String? { diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 137d3780e8..21419d55cf 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -83,7 +83,6 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.receivers.DebugReceiver import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import reactivecircus.flowbinding.android.view.clicks diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt index 85f0a8ba2d..5e4528d381 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.BuildConfig import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.command.Command diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 983a094fa3..2017e2d877 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -16,7 +16,6 @@ package im.vector.app.features.command -import im.vector.app.BuildConfig import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.features.home.room.detail.ChatEffect diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 1f0f796cf7..34afe81c3e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -68,7 +68,6 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vanniktech.emoji.EmojiPopup -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 563ab00d18..f8d7fe7628 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -282,6 +282,7 @@ class TimelineViewModel @AssistedInject constructor( copy(myRoomMember = it) } } + private fun setupPreviewUrlObservers() { if (!vectorPreferences.showUrlPreviews()) { return @@ -488,6 +489,7 @@ class TimelineViewModel @AssistedInject constructor( val content = initialState.rootThreadEventId?.let { action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.IO_THREAD, it)) } ?: action.stickerContent + room.sendEvent(EventType.STICKER, content.toContent()) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 8e10ea94a6..7e83cbc174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 4a7f1a5e03..64701323ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -20,7 +20,6 @@ import dagger.Lazy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 29ecdd361e..815202a4e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.extensions.localDateTime import im.vector.app.core.resources.UserPreferencesProvider -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent 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 484309571f..4bd84ae603 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 @@ -29,7 +29,6 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 7a3e0033e3..118e820f84 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -48,6 +48,5 @@ class VectorSettingsLabsFragment @Inject constructor( false } } - } } From b1b27bdd0ed9f2b6afbb8c1ad3214ad892de142d Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 25 Jan 2022 14:12:13 +0200 Subject: [PATCH 105/130] Enhance edit to support new threads fallback --- .../sdk/api/session/events/model/Event.kt | 3 ++- .../room/model/relation/ReplyToContent.kt | 2 ++ .../EventRelationsAggregationProcessor.kt | 2 +- .../composer/MessageComposerViewModel.kt | 20 ++++++++++++++++--- 4 files changed, 22 insertions(+), 5 deletions(-) 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 166c24d19a..7c8739d821 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 @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent 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.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.threads.ThreadDetails import org.matrix.android.sdk.api.util.ContentUtils @@ -376,7 +377,7 @@ fun Event.isReply(): Boolean { } fun Event.isReplyRenderedInThread(): Boolean { - return isReply() && getRelationContent()?.inReplyTo?.renderIn?.contains("m.thread") == true + return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true } fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt index 91bd5edc60..412a1bfca9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt @@ -24,3 +24,5 @@ data class ReplyToContent( @Json(name = "event_id") val eventId: String? = null, @Json(name = "render_in") val renderIn: List? = null ) + +fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 62b6d626f5..e1d2ed4e6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -64,7 +64,7 @@ import javax.inject.Inject internal class EventRelationsAggregationProcessor @Inject constructor( @UserId private val userId: String, private val stateEventDataSource: StateEventDataSource -) : EventInsertLiveProcessor { + ) : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index b7425af7c9..39c43f9709 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -51,6 +51,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent @@ -185,7 +186,7 @@ class MessageComposerViewModel @AssistedInject constructor( is SendMode.Regular -> { when (val slashCommandResult = CommandParser.parseSlashCommand( textMessage = action.text, - isInThreadTimeline = state.isInThreadTimeline())) { + isInThreadTimeline = state.isInThreadTimeline())) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room if (state.rootThreadEventId != null) { @@ -259,7 +260,7 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(slashCommandResult) } - is ParsedCommand.RemoveUser -> { + is ParsedCommand.RemoveUser -> { handleRemoveSlashCommand(slashCommandResult) } is ParsedCommand.JoinRoom -> { @@ -449,7 +450,20 @@ class MessageComposerViewModel @AssistedInject constructor( } is SendMode.Edit -> { // is original event a reply? - val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId + val relationContent = state.sendMode.timelineEvent.getRelationContent() + val inReplyTo = if (state.rootThreadEventId != null) { + if (relationContent?.inReplyTo?.shouldRenderInThread() == true) { + // Reply within a thread event + relationContent.inReplyTo?.eventId + } else { + // Normal thread event + null + } + } else { + // Normal event + relationContent?.inReplyTo?.eventId + } + if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { From c19b52cded7c4968372ca81709c5ff93e53ad061 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 25 Jan 2022 18:21:42 +0200 Subject: [PATCH 106/130] Enhance thread summary Fix deleted root thread messages when show deleted messages are enabled/disabled --- .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../database/helper/ThreadEventsHelper.kt | 68 +++++++++++++++---- .../internal/database/mapper/EventMapper.kt | 1 + .../room/prune/RedactionEventProcessor.kt | 1 + .../helper/TimelineEventVisibilityHelper.kt | 14 +++- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index 26e8688d34..dc62d4ae35 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -28,5 +28,6 @@ data class ThreadDetails( val threadSummarySenderInfo: SenderInfo? = null, val threadSummaryLatestTextMessage: String? = null, val lastMessageTimestamp: Long? = null, - var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE + var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE, + val isThread: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 33829712c7..b54f6b3ecf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.RealmQuery -import io.realm.RealmResults import io.realm.Sort import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -27,10 +26,14 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.findIncludingEvent +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +private typealias ThreadSummary = Pair? + /** * Finds the root thread event and update it with the latest message summary along with the number * of threads included. If there is no root thread event no action is done @@ -39,19 +42,21 @@ internal fun Map.updateThreadSummaryIfNeeded( roomId: String, realm: Realm, currentUserId: String, shouldUpdateNotifications: Boolean = true) { - for ((rootThreadEventId, eventEntity) in this) { - eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let { - if (it.isNullOrEmpty()) return@let - val latestMessage = it.firstOrNull() + for ((rootThreadEventId, eventEntity) in this) { + eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId)?.let { threadSummary -> + + val numberOfMessages = threadSummary.first + val latestEventInThread = threadSummary.second // If this is a thread message, find its root event if exists val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity rootThreadEvent?.markEventAsRoot( - threadsCounted = it.size, - latestMessageTimelineEventEntity = latestMessage + threadsCounted = numberOfMessages, + latestMessageTimelineEventEntity = latestEventInThread ) + } } @@ -82,16 +87,49 @@ internal fun EventEntity.markEventAsRoot( threadSummaryLatestMessage = latestMessageTimelineEventEntity } +///** +// * Find all TimelineEventEntity that are threads bind to the Event with rootThreadEventId +// * @param rootThreadEventId The root eventId that will try to find bind threads +// */ +//internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEventId: String): RealmResults = +// TimelineEventEntity +// .whereRoomId(realm, roomId = roomId) +// .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) +// .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) +// .findAll() + /** - * Find all TimelineEventEntity that are threads bind to the Event with rootThreadEventId - * @param rootThreadEventId The root eventId that will try to find bind threads + * Count the number of threads for the provided root thread eventId, and finds the latest event message + * @param rootThreadEventId The root eventId that will find the number of threads + * @return A ThreadSummary containing the counted threads and the latest event message */ -internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEventId: String): RealmResults = - TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .findAll() +internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String): ThreadSummary { + + // Number of messages + val messages = TimelineEventEntity + .whereRoomId(realm, roomId = roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) + .count() + .toInt() + + if (messages <= 0) return null + + // Find latest thread event, we know it exists + var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return null + var result: TimelineEventEntity? = null + + // Iterate the chunk until we find our latest event + while (result == null) { + result = chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull { + it.root?.rootThreadEventId == rootThreadEventId + } + chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break + } + + result ?: return null + + return ThreadSummary(messages, result) +} /** * Find all TimelineEventEntity that are root threads for the specified room 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 c10b9084ea..9c420e81fd 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 @@ -103,6 +103,7 @@ internal object EventMapper { it.mCryptoErrorReason = eventEntity.decryptionErrorReason it.threadDetails = ThreadDetails( isRootThread = eventEntity.isRootThread, + isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(), numberOfThreads = eventEntity.numberOfThreads, threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity -> SenderInfo( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index 5ae4007c63..ac06d674d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -21,6 +21,7 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.api.session.room.model.relation.RelationContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EventEntity diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 815202a4e7..2cde1e0ef8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel 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.timeline.TimelineEvent +import timber.log.Timber import javax.inject.Inject class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { @@ -137,9 +138,19 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?, isFromThreadTimeline: Boolean): Boolean { - if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { + + if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages() && root.threadDetails?.isRootThread == false ) { return true } + + // We should not display deleted thread messages within the normal timeline + if (root.isRedacted() && + userPreferencesProvider.areThreadMessagesEnabled() && + root.threadDetails?.isThread == true){ + return true + } + + if (root.getRelationContent()?.type == RelationType.REPLACE) { return true } @@ -155,6 +166,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen return true } + // Allow only the the threads within the rootThreadEventId along with the root event if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) { return if (root.getRootThreadEventId() == rootThreadEventId) { false From 92d082c26abfe1b70ef91b3de45176a5d7e4041d Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 26 Jan 2022 14:07:07 +0200 Subject: [PATCH 107/130] Improve thread message deletion Fix thread summary after isLimited events --- .../android/sdk/api/session/events/model/Event.kt | 15 ++++++++------- .../database/helper/ThreadEventsHelper.kt | 8 ++++---- .../session/room/prune/RedactionEventProcessor.kt | 4 +++- .../room/timeline/TokenChunkEventPersistor.kt | 5 ++++- .../session/sync/handler/room/RoomSyncHandler.kt | 3 +-- .../helper/TimelineEventVisibilityHelper.kt | 11 +++++++++-- 6 files changed, 29 insertions(+), 17 deletions(-) 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 7c8739d821..57c083e757 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 @@ -200,16 +200,17 @@ data class Event( * It will return a decrypted text message or an empty string otherwise. */ fun getDecryptedTextSummary(): String? { + if (isRedacted()) return "Message Deleted" val text = getDecryptedValue() ?: return null return when { isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) - isFileMessage() -> "sent a file." - isAudioMessage() -> "sent an audio file." - isImageMessage() -> "sent an image." - isVideoMessage() -> "sent a video." - isSticker() -> "sent a sticker" - isPoll() -> getPollQuestion() ?: "created a poll." - else -> text + isFileMessage() -> "sent a file." + isAudioMessage() -> "sent an audio file." + isImageMessage() -> "sent an image." + isVideoMessage() -> "sent a video." + isSticker() -> "sent a sticker" + isPoll() -> getPollQuestion() ?: "created a poll." + else -> text } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index b54f6b3ecf..cb83094c73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -41,10 +41,11 @@ private typealias ThreadSummary = Pair? internal fun Map.updateThreadSummaryIfNeeded( roomId: String, realm: Realm, currentUserId: String, + chunkEntity: ChunkEntity? = null, shouldUpdateNotifications: Boolean = true) { for ((rootThreadEventId, eventEntity) in this) { - eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId)?.let { threadSummary -> + eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary -> val numberOfMessages = threadSummary.first val latestEventInThread = threadSummary.second @@ -103,7 +104,7 @@ internal fun EventEntity.markEventAsRoot( * @param rootThreadEventId The root eventId that will find the number of threads * @return A ThreadSummary containing the counted threads and the latest event message */ -internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String): ThreadSummary { +internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary { // Number of messages val messages = TimelineEventEntity @@ -115,7 +116,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: if (messages <= 0) return null // Find latest thread event, we know it exists - var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return null + var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null var result: TimelineEventEntity? = null // Iterate the chunk until we find our latest event @@ -125,7 +126,6 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: } chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break } - result ?: return null return ThreadSummary(messages, result) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index ac06d674d0..108f159e1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -84,7 +84,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr // } val modified = unsignedData.copy(redactedEvent = redactionEvent) - eventToPrune.content = ContentMapper.map(emptyMap()) + // I Commented the line below, it should not be empty while we lose all the previous info about + // the redacted event +// eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) eventToPrune.decryptionResultJson = null eventToPrune.decryptionErrorCode = null 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 387085724c..e43f3fd240 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 @@ -200,7 +200,10 @@ internal class TokenChunkEventPersistor @Inject constructor( } if (lightweightSettingsStorage.areThreadMessagesEnabled()) { - optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId) + optimizedThreadSummaryMap.updateThreadSummaryIfNeeded( + roomId = roomId, + realm = realm, + currentUserId = userId) } } } 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 f4f8cc01c6..640fe53727 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 @@ -416,7 +416,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity } } - // Give info to crypto module cryptoService.onLiveEvent(roomEntity.roomId, event) @@ -443,11 +442,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } // Handle deletion of [stuck] local echos if needed deleteLocalEchosIfNeeded(insertType, roomEntity, eventList) - if (lightweightSettingsStorage.areThreadMessagesEnabled()) { optimizedThreadSummaryMap.updateThreadSummaryIfNeeded( roomId = roomId, realm = realm, + chunkEntity = chunkEntity, currentUserId = userId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 2cde1e0ef8..561f36ce41 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -146,10 +146,17 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen // We should not display deleted thread messages within the normal timeline if (root.isRedacted() && userPreferencesProvider.areThreadMessagesEnabled() && - root.threadDetails?.isThread == true){ + !isFromThreadTimeline && + (root.isThread() || root.threadDetails?.isThread == true)){ + return true + } + if (root.isRedacted() && + !userPreferencesProvider.shouldShowRedactedMessages() && + userPreferencesProvider.areThreadMessagesEnabled() && + isFromThreadTimeline && + root.isThread()){ return true } - if (root.getRelationContent()?.type == RelationType.REPLACE) { return true From 358a7d0ec41eee83adab2fe5134023b905d68814 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 27 Jan 2022 13:22:04 +0200 Subject: [PATCH 108/130] Handle latest thread message & root thread edition to update thread summary and thread list appropriately --- .../session/room/timeline/TimelineService.kt | 7 ++++ .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../database/helper/ThreadEventsHelper.kt | 35 ++++++++++++------- .../EventRelationsAggregationProcessor.kt | 26 +++++++++++++- .../room/timeline/DefaultTimelineService.kt | 7 ++++ .../list/viewmodel/ThreadListController.kt | 3 +- .../list/viewmodel/ThreadListViewModel.kt | 1 + 7 files changed, 66 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index aefda755f1..2899e98da8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -86,6 +86,13 @@ interface TimelineService { */ fun isUserParticipatingInThread(rootThreadEventId: String): Boolean + /** + * Enhance the thread list with the edited events if needed + * @return the [LiveData] of [TimelineEvent] + */ + fun mapEventsWithEdition(threads: List): List + + /** * Marks the current thread as read. This is a local implementation * @param rootThreadEventId the eventId of the current thread diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index dc62d4ae35..fafe17b2c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -29,5 +29,6 @@ data class ThreadDetails( val threadSummaryLatestTextMessage: String? = null, val lastMessageTimestamp: Long? = null, var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE, - val isThread: Boolean = false + val isThread: Boolean = false, + val lastRootThreadEdition: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index cb83094c73..b0cbc607ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -19,9 +19,11 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity @@ -88,17 +90,6 @@ internal fun EventEntity.markEventAsRoot( threadSummaryLatestMessage = latestMessageTimelineEventEntity } -///** -// * Find all TimelineEventEntity that are threads bind to the Event with rootThreadEventId -// * @param rootThreadEventId The root eventId that will try to find bind threads -// */ -//internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEventId: String): RealmResults = -// TimelineEventEntity -// .whereRoomId(realm, roomId = roomId) -// .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) -// .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) -// .findAll() - /** * Count the number of threads for the provided root thread eventId, and finds the latest event message * @param rootThreadEventId The root eventId that will find the number of threads @@ -124,7 +115,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: result = chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull { it.root?.rootThreadEventId == rootThreadEventId } - chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break + chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break } result ?: return null @@ -139,7 +130,25 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, TimelineEventEntity .whereRoomId(realm, roomId = roomId) .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) - .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.DISPLAY_INDEX}", Sort.DESCENDING) + .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING) + +internal fun List.mapEventsWithEdition(realm: Realm, roomId: String): List = + this.map { + EventAnnotationsSummaryEntity + .where(realm, roomId, eventId = it.eventId) + .findFirst() + ?.editSummary + ?.editions + ?.lastOrNull() + ?.eventId + ?.let { editedEventId -> + TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent -> + it.root.threadDetails = it.root.threadDetails?.copy(lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() + ?: "(edited)") + it + } ?: it + } ?: it + } /** * Find the number of all the local notifications for the specified room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index e1d2ed4e6b..56704474bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.verification.toState +import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity @@ -64,7 +65,7 @@ import javax.inject.Inject internal class EventRelationsAggregationProcessor @Inject constructor( @UserId private val userId: String, private val stateEventDataSource: StateEventDataSource - ) : EventInsertLiveProcessor { +) : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, @@ -302,6 +303,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor( ) } } + + if(!isLocalEcho) { + val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions) + } + } + + /** + * Check if the edition is on the latest thread event, and update it accordingly + */ + private fun handleThreadSummaryEdition(editedEvent: EventEntity?, + replaceEvent: TimelineEventEntity?, + editions: List?) { + replaceEvent ?: return + editedEvent ?: return + editedEvent.findRootThreadEvent()?.apply { + val threadSummaryEventId = threadSummaryLatestMessage?.eventId + if (editedEvent.eventId == threadSummaryEventId || editions?.any { it.eventId == threadSummaryEventId } == true) { + // The edition is for the latest event or for any event replaced, this is to handle multiple + // edits of the same latest event + threadSummaryLatestMessage = replaceEvent + } + } } private fun handleResponse(realm: Realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 200de10e86..cc6485698b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread +import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.EventEntity @@ -157,6 +158,12 @@ internal class DefaultTimelineService @AssistedInject constructor( } } + override fun mapEventsWithEdition(threads: List): List { + return Realm.getInstance(monarchy.realmConfiguration).use { + threads.mapEventsWithEdition(it, roomId) + } + } + override suspend fun markThreadAsRead(rootThreadEventId: String) { monarchy.awaitTransaction { EventEntity.where( diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index c123bceafb..8bc6bd73e9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -60,6 +60,7 @@ class ThreadListController @Inject constructor( ?.forEach { timelineEvent -> val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST) val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) + val lastRootThreadEdition = timelineEvent.root.threadDetails?.lastRootThreadEdition threadListItem { id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) @@ -68,7 +69,7 @@ class ThreadListController @Inject constructor( date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage) + rootMessage(lastRootThreadEdition ?: timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage ?: decryptionErrorMessage) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 88751004a4..d82b5d6ccf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -61,6 +61,7 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState private fun observeThreadsList() { room?.flow() ?.liveThreadList() + ?.map { room.mapEventsWithEdition(it) } ?.map { it.map { threadRootEvent -> val isParticipating = room.isUserParticipatingInThread(threadRootEvent.eventId) From f53b711e0d9b9068e919217ecf7e0643eb793e0a Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 27 Jan 2022 13:49:03 +0200 Subject: [PATCH 109/130] When thread disabled add thread replies within threads ( to the users with threads enabled ) --- .../room/model/relation/RelationService.kt | 7 ++++++- .../database/helper/ThreadEventsHelper.kt | 3 +++ .../room/relation/DefaultRelationService.kt | 16 +++++++++++++--- .../session/room/relation/EventEditor.kt | 4 +++- .../session/room/send/LocalEchoEventFactory.kt | 12 ++++++++---- .../detail/composer/MessageComposerViewModel.kt | 13 ++++++++++++- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index e49b1f0a73..a9c5ba19a8 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 @@ -109,10 +109,15 @@ interface RelationService { * @param eventReplied the event referenced by the reply * @param replyText the reply text * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param showInThread If true, relation will be added to the reply in order to be visible from within threads + * @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation */ fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, - autoMarkdown: Boolean = false): Cancelable? + autoMarkdown: Boolean = false, + showInThread: Boolean = false, + rootThreadEventId: String? = null + ): Cancelable? /** * Get the current EventAnnotationsSummary diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index b0cbc607ca..806b07316d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -132,6 +132,9 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING) +/** + * Map each timelineEvent with the equivalent decrypted text edition/replacement for root threads + */ internal fun List.mapEventsWithEdition(realm: Realm, roomId: String): List = this.map { EventAnnotationsSummaryEntity 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 08154b9cbd..ac5e90733b 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 @@ -137,12 +137,20 @@ internal class DefaultRelationService @AssistedInject constructor( return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId)) } - override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? { + override fun replyToMessage( + eventReplied: TimelineEvent, + replyText: CharSequence, + autoMarkdown: Boolean, + showInThread: Boolean, + rootThreadEventId: String? + ): Cancelable? { val event = eventFactory.createReplyTextEvent( roomId = roomId, eventReplied = eventReplied, replyText = replyText, - autoMarkdown = autoMarkdown) + autoMarkdown = autoMarkdown, + rootThreadEventId = rootThreadEventId, + showInThread = showInThread) ?.also { saveLocalEcho(it) } ?: return null @@ -182,7 +190,9 @@ internal class DefaultRelationService @AssistedInject constructor( eventReplied = eventReplied, replyText = replyInThreadText, autoMarkdown = autoMarkdown, - rootThreadEventId = rootThreadEventId) + rootThreadEventId = rootThreadEventId, + showInThread = false + ) ?.also { saveLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index 7e3d7dfde8..ea94b93767 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -71,7 +71,9 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: roomId = roomId, eventReplied = originalTimelineEvent, replyText = newBodyText, - autoMarkdown = false)?.copy( + autoMarkdown = false, + showInThread = false + )?.copy( eventId = replyToEdit.eventId ) ?: return NoOpCancellable updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 29ab2c7f0b..17c396b724 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 @@ -469,7 +469,8 @@ internal class LocalEchoEventFactory @Inject constructor( eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean, - rootThreadEventId: String? = null): Event? { + rootThreadEventId: String? = null, + showInThread: Boolean): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null @@ -500,7 +501,10 @@ internal class LocalEchoEventFactory @Inject constructor( format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, formattedBody = replyFormatted, - relatesTo = generateReplyRelationContent(eventId = eventId, rootThreadEventId = rootThreadEventId)) + relatesTo = generateReplyRelationContent( + eventId = eventId, + rootThreadEventId = rootThreadEventId, + showAsReply = showInThread )) return createMessageEvent(roomId, content) } @@ -516,12 +520,12 @@ internal class LocalEchoEventFactory @Inject constructor( * } * } */ - private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null): RelationDefaultContent = + private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent = rootThreadEventId?.let { RelationDefaultContent( type = RelationType.IO_THREAD, eventId = it, - inReplyTo = ReplyToContent(eventId = eventId, renderIn = arrayListOf("m.thread"))) + inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null)) } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId)) private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 39c43f9709..766417c320 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -26,6 +26,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand @@ -44,6 +45,8 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData 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.isThread 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 @@ -495,13 +498,21 @@ class MessageComposerViewModel @AssistedInject constructor( } is SendMode.Reply -> { val timelineEvent = state.sendMode.timelineEvent + val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null + val rootThreadEventId = if(showInThread) timelineEvent.root.getRootThreadEventId() else null state.rootThreadEventId?.let { room.replyInThread( rootThreadEventId = it, replyInThreadText = action.text.toString(), autoMarkdown = action.autoMarkdown, eventReplied = timelineEvent) - } ?: room.replyToMessage(timelineEvent, action.text.toString(), action.autoMarkdown) + } ?: room.replyToMessage( + eventReplied = timelineEvent, + replyText = action.text.toString(), + autoMarkdown = action.autoMarkdown, + showInThread = showInThread, + rootThreadEventId = rootThreadEventId + ) _viewEvents.post(MessageComposerViewEvents.MessageSent) popDraft() From 554ece724eb3cc50fff9dcf218835e463bc2d71f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 27 Jan 2022 14:55:34 +0200 Subject: [PATCH 110/130] - Remove counter from thread notifications - Fix red dot on user mentioning --- .../matrix/android/sdk/api/session/events/model/Event.kt | 7 +++++++ .../sdk/internal/database/helper/ThreadEventsHelper.kt | 3 +-- .../src/main/res/layout/view_thread_notification_badge.xml | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) 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 57c083e757..df57ca5681 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 @@ -219,6 +219,13 @@ data class Event( return getDecryptedValue("formatted_body")?.contains("
") ?: false } + /** + * Determines whether or not current event has mentioned the user + */ + fun isUserMentioned(userId: String): Boolean { + return getDecryptedValue("formatted_body")?.contains(userId) ?: false + } + /** * Decrypt the message, or return the pure payload value if there is no encryption */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 806b07316d..4f8643bc9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -210,8 +210,7 @@ internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): St * Returns whether or not the user is mentioned in the event */ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean { - val decryptedContent = timelineEventEntity?.root?.asDomain()?.getDecryptedTextSummary().orEmpty() - return decryptedContent.contains(currentUserId.replace("@", "").substringBefore(":")) + return timelineEventEntity?.root?.asDomain()?.isUserMentioned(currentUserId) == true } /** diff --git a/vector/src/main/res/layout/view_thread_notification_badge.xml b/vector/src/main/res/layout/view_thread_notification_badge.xml index 8e2e098d7b..81b3f7138e 100644 --- a/vector/src/main/res/layout/view_thread_notification_badge.xml +++ b/vector/src/main/res/layout/view_thread_notification_badge.xml @@ -26,12 +26,11 @@ From b83872d5f09e9b8206fdcf7168b7fb62917be6dd Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 27 Jan 2022 16:38:14 +0200 Subject: [PATCH 111/130] When show all threads developer mode option is enabled, prevent reply in thread to those events --- .../home/room/detail/timeline/action/MessageActionsViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 64701323ec..339fcf13a7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage +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.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat @@ -448,6 +449,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted actionPermissions: ActionPermissions): Boolean { if (!vectorPreferences.areThreadMessagesEnabled()) return false if (initialState.isFromThreadTimeline) return false + if (event.root.isThread()) return false if (event.root.getClearType() != EventType.MESSAGE && !event.isSticker() && !event.isPoll()) return false if (!actionPermissions.canSendMessage) return false From bac6d271ca9dc4e9014b1ef0cf6e515ae34e2f7e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 27 Jan 2022 18:13:05 +0200 Subject: [PATCH 112/130] Merge develop into this branch --- .../app/core/resources/UserPreferencesProvider.kt | 4 ---- .../im/vector/app/features/command/ParsedCommand.kt | 2 +- .../app/features/home/room/detail/TimelineFragment.kt | 10 ++++++---- .../home/room/detail/arguments/TimelineArgs.kt | 3 ++- .../app/features/home/room/threads/ThreadsActivity.kt | 7 +------ .../home/room/threads/list/views/ThreadListFragment.kt | 2 +- .../src/main/res/layout/view_room_detail_toolbar.xml | 10 ++++------ 7 files changed, 15 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index 77e773c781..3aa1964d8d 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -49,10 +49,6 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: return vectorPreferences.showAvatarDisplayNameChangeMessages() } - fun shouldShowPolls(): Boolean { - return vectorPreferences.labsEnablePolls() - } - fun areThreadMessagesEnabled(): Boolean { return vectorPreferences.areThreadMessagesEnabled() } diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 611d11a222..590e8786d0 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -28,7 +28,7 @@ sealed interface ParsedCommand { object ErrorEmptySlashCommand : ParsedCommand - class ErrorCommandNotSupportedInThreads(val slashCommand: String) : ParsedCommand() + class ErrorCommandNotSupportedInThreads(val slashCommand: String) : ParsedCommand // Unknown/Unsupported slash command data class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 4f3d93e0ac..aa33550b5e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -614,7 +614,7 @@ class TimelineFragment @Inject constructor( navigator .openLocationSharing( context = requireContext(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mode = LocationSharingMode.PREVIEW, initialLocationData = viewEvent.locationData, locationOwnerId = viewEvent.userId @@ -1470,7 +1470,9 @@ class TimelineFragment @Inject constructor( attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) attachmentTypeSelector.setAttachmentVisibility( AttachmentTypeSelectorView.Type.LOCATION, - vectorPreferences.isLocationSharingEnabled() && !isThreadTimeLine()) + vectorPreferences.isLocationSharingEnabled()) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -2172,7 +2174,7 @@ class TimelineFragment @Inject constructor( } is EventSharedAction.Edit -> { if (action.eventType == EventType.POLL_START) { - navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, action.eventId, PollMode.EDIT) + navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT) } else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } else { @@ -2437,7 +2439,7 @@ class TimelineFragment @Inject constructor( navigator .openLocationSharing( context = requireContext(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mode = LocationSharingMode.STATIC_SHARING, initialLocationData = null, locationOwnerId = session.myUserId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt index 26455e04c7..f22fe1b7df 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt @@ -27,5 +27,6 @@ data class TimelineArgs( val eventId: String? = null, val sharedData: SharedData? = null, val openShareSpaceForId: String? = null, - val threadTimelineArgs: ThreadTimelineArgs? = null + val threadTimelineArgs: ThreadTimelineArgs? = null, + val switchToParentSpace: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index b9d77a323a..b0ecb2db5d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -25,7 +25,6 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityThreadsBinding import im.vector.app.features.home.AvatarRenderer @@ -38,7 +37,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @AndroidEntryPoint -class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable { +class ThreadsActivity : VectorBaseActivity() { @Inject lateinit var avatarRenderer: AvatarRenderer @@ -120,10 +119,6 @@ class ThreadsActivity : VectorBaseActivity(), ToolbarCon ) } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - /** * Determine in witch fragment we should navigate */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index f388ce1410..180e6226d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -87,7 +87,7 @@ class ThreadListFragment @Inject constructor( } private fun initToolbar() { - setupToolbar(views.threadListToolbar) + setupToolbar(views.threadListToolbar).allowBack() renderToolbar() } diff --git a/vector/src/main/res/layout/view_room_detail_toolbar.xml b/vector/src/main/res/layout/view_room_detail_toolbar.xml index ab78f45243..4a534ce867 100644 --- a/vector/src/main/res/layout/view_room_detail_toolbar.xml +++ b/vector/src/main/res/layout/view_room_detail_toolbar.xml @@ -3,9 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/roomToolbarContentView" - tools:visibility="visible" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + tools:visibility="visible"> Date: Thu, 27 Jan 2022 18:39:43 +0200 Subject: [PATCH 113/130] ktlint format --- .../sdk/api/session/room/timeline/TimelineService.kt | 1 - .../sdk/internal/database/helper/ThreadEventsHelper.kt | 3 --- .../session/room/EventRelationsAggregationProcessor.kt | 2 +- .../session/room/prune/RedactionEventProcessor.kt | 1 - .../internal/session/room/send/LocalEchoEventFactory.kt | 2 +- .../home/room/detail/composer/MessageComposerViewModel.kt | 3 +-- .../features/home/room/detail/search/SearchResultItem.kt | 2 +- .../timeline/helper/TimelineEventVisibilityHelper.kt | 8 +++----- .../app/features/home/room/threads/ThreadsActivity.kt | 1 - 9 files changed, 7 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 2899e98da8..c9ccf9ca51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -92,7 +92,6 @@ interface TimelineService { */ fun mapEventsWithEdition(threads: List): List - /** * Marks the current thread as read. This is a local implementation * @param rootThreadEventId the eventId of the current thread diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 4f8643bc9e..fb1d2b381d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -45,7 +45,6 @@ internal fun Map.updateThreadSummaryIfNeeded( realm: Realm, currentUserId: String, chunkEntity: ChunkEntity? = null, shouldUpdateNotifications: Boolean = true) { - for ((rootThreadEventId, eventEntity) in this) { eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary -> @@ -59,7 +58,6 @@ internal fun Map.updateThreadSummaryIfNeeded( threadsCounted = numberOfMessages, latestMessageTimelineEventEntity = latestEventInThread ) - } } @@ -96,7 +94,6 @@ internal fun EventEntity.markEventAsRoot( * @return A ThreadSummary containing the counted threads and the latest event message */ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary { - // Number of messages val messages = TimelineEventEntity .whereRoomId(realm, roomId = roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 89d34cba1b..acceaf6e24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -334,7 +334,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - if(!isLocalEcho) { + if (!isLocalEcho) { val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index 108f159e1f..ee52fe574b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -21,7 +21,6 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData -import org.matrix.android.sdk.api.session.room.model.relation.RelationContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EventEntity 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 0e8223e51d..3c36d58710 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 @@ -560,7 +560,7 @@ internal class LocalEchoEventFactory @Inject constructor( relatesTo = generateReplyRelationContent( eventId = eventId, rootThreadEventId = rootThreadEventId, - showAsReply = showInThread )) + showAsReply = showInThread)) return createMessageEvent(roomId, content) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index c2b89a4246..b573fd3305 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -26,7 +26,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.attachments.toContentAttachmentData @@ -503,7 +502,7 @@ class MessageComposerViewModel @AssistedInject constructor( is SendMode.Reply -> { val timelineEvent = state.sendMode.timelineEvent val showInThread = state.sendMode.timelineEvent.root.isThread() && state.rootThreadEventId == null - val rootThreadEventId = if(showInThread) timelineEvent.root.getRootThreadEventId() else null + val rootThreadEventId = if (showInThread) timelineEvent.root.getRootThreadEventId() else null state.rootThreadEventId?.let { room.replyInThread( rootThreadEventId = it, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 7ed1a66ad6..2ec786fab2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -30,8 +30,8 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.threads.ThreadDetails import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import org.matrix.android.sdk.api.session.threads.ThreadDetails import org.matrix.android.sdk.api.util.MatrixItem @EpoxyModelClass(layout = R.layout.item_search_result) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index ad422febe1..7656a671f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -27,7 +27,6 @@ import org.matrix.android.sdk.api.session.events.model.toModel 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.timeline.TimelineEvent -import timber.log.Timber import javax.inject.Inject class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { @@ -138,8 +137,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?, isFromThreadTimeline: Boolean): Boolean { - - if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages() && root.threadDetails?.isRootThread == false ) { + if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages() && root.threadDetails?.isRootThread == false) { return true } @@ -147,14 +145,14 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen if (root.isRedacted() && userPreferencesProvider.areThreadMessagesEnabled() && !isFromThreadTimeline && - (root.isThread() || root.threadDetails?.isThread == true)){ + (root.isThread() || root.threadDetails?.isThread == true)) { return true } if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages() && userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline && - root.isThread()){ + root.isThread()) { return true } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index b0ecb2db5d..ca18060c51 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -20,7 +20,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.FragmentTransaction -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragmentToBackstack From 1d6d8102b36a16a2eeabf8f1f389a799b0a926f2 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 28 Jan 2022 14:11:03 +0200 Subject: [PATCH 114/130] Further improve thread summary after forward scrolling --- .../database/helper/ThreadEventsHelper.kt | 36 +++++++++++++++++-- .../room/timeline/TokenChunkEventPersistor.kt | 4 ++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index fb1d2b381d..9a8911a631 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.database.query.findIncludingEvent import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +import timber.log.Timber private typealias ThreadSummary = Pair? @@ -109,16 +110,45 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: // Iterate the chunk until we find our latest event while (result == null) { - result = chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull { - it.root?.rootThreadEventId == rootThreadEventId - } + result = findLatestSortedChunkEvent(chunk, rootThreadEventId) chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break } + + if (result == null && chunkEntity != null) { + // Find latest event from our current chunk + result = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId) + } else if (result != null && chunkEntity != null) { + val currentChunkLatestEvent = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId) + result = findMostRecentEvent(result, currentChunkLatestEvent) + } + result ?: return null return ThreadSummary(messages, result) } +/** + * Lets compare them in case user is moving forward in the timeline and we cannot know the + * exact chunk sequence while currentChunk is not yet committed in the DB + */ +private fun findMostRecentEvent(result: TimelineEventEntity, currentChunkLatestEvent: TimelineEventEntity?): TimelineEventEntity { + currentChunkLatestEvent ?: return result + val currentChunkEventTimestamp = currentChunkLatestEvent.root?.originServerTs ?: return result + val resultTimestamp = result.root?.originServerTs ?: return result + if (currentChunkEventTimestamp > resultTimestamp) { + return currentChunkLatestEvent + } + return result +} + +/** + * Find the latest event of the current chunk + */ +private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: String): TimelineEventEntity? = + chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull { + it.root?.rootThreadEventId == rootThreadEventId + } + /** * Find all TimelineEventEntity that are root threads for the specified room * @param roomId The room that all stored root threads will be returned 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 e43f3fd240..6607e71bd9 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 @@ -203,7 +203,9 @@ internal class TokenChunkEventPersistor @Inject constructor( optimizedThreadSummaryMap.updateThreadSummaryIfNeeded( roomId = roomId, realm = realm, - currentUserId = userId) + currentUserId = userId, + chunkEntity = currentChunk + ) } } } From b1067e9a588b0982e92be6ac68422bb9dabd89a8 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Fri, 28 Jan 2022 16:37:59 +0200 Subject: [PATCH 115/130] - ktlint format - Update a text resource --- .../android/sdk/internal/database/helper/ThreadEventsHelper.kt | 1 - .../session/sync/handler/room/ThreadsAwarenessHandler.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 9a8911a631..afc090604a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -33,7 +33,6 @@ import org.matrix.android.sdk.internal.database.query.findIncludingEvent import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId -import timber.log.Timber private typealias ThreadSummary = Pair? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 67aade1d0c..f3a1523955 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -308,7 +308,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( eventEntity: EventEntity?, eventPayload: MutableMap): String? { val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format( - "Replied within a thread", + "In reply to a thread", eventBody) val messageTextContent = MessageTextContent( From cdd36ce034df8b1c061bd5f78902fb5ac6a5bcb6 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 11:56:26 +0200 Subject: [PATCH 116/130] Fix IndexOutOfBound crashes while clicking permalinks --- .../timeline/helper/TimelineControllerInterceptorHelper.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 7165921b35..8a0e1e18fd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -115,7 +115,10 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut private fun MutableList>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false if (shouldAddForwardPrefetch) { - val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1) + val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD + .coerceAtMost(size - 1) + .coerceAtLeast(0) + val loadingItem = LoadingItem_() .id("prefetch_forward_loading${System.currentTimeMillis()}") .showLoader(false) From 32a982c2877df9374bc85d9a10aafc19b9ad1f36 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 12:19:12 +0200 Subject: [PATCH 117/130] Improve coerceIn format --- .../timeline/helper/TimelineControllerInterceptorHelper.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 8a0e1e18fd..8a36131cc9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -100,8 +100,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false if (shouldAddBackwardPrefetch) { val indexOfPrefetchBackward = (previousModelsSize - 1) - .coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD) - .coerceAtLeast(0) + .coerceIn(minimumValue = 0, maximumValue = size - DEFAULT_PREFETCH_THRESHOLD) val loadingItem = LoadingItem_() .id("prefetch_backward_loading${System.currentTimeMillis()}") @@ -116,8 +115,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false if (shouldAddForwardPrefetch) { val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD - .coerceAtMost(size - 1) - .coerceAtLeast(0) + .coerceIn(minimumValue = 0, maximumValue = size - 1) val loadingItem = LoadingItem_() .id("prefetch_forward_loading${System.currentTimeMillis()}") From 5ff5f762d417bbce9734c94333c4dc3bf52d7b6e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 12:24:28 +0200 Subject: [PATCH 118/130] Revert the use of coerceIn --- .../timeline/helper/TimelineControllerInterceptorHelper.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 8a36131cc9..8a0e1e18fd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -100,7 +100,8 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false if (shouldAddBackwardPrefetch) { val indexOfPrefetchBackward = (previousModelsSize - 1) - .coerceIn(minimumValue = 0, maximumValue = size - DEFAULT_PREFETCH_THRESHOLD) + .coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD) + .coerceAtLeast(0) val loadingItem = LoadingItem_() .id("prefetch_backward_loading${System.currentTimeMillis()}") @@ -115,7 +116,8 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false if (shouldAddForwardPrefetch) { val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD - .coerceIn(minimumValue = 0, maximumValue = size - 1) + .coerceAtMost(size - 1) + .coerceAtLeast(0) val loadingItem = LoadingItem_() .id("prefetch_forward_loading${System.currentTimeMillis()}") From 3253a252fb0f237af9137733d32a68fd6697bf6e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 12:58:19 +0200 Subject: [PATCH 119/130] Introduce ThreadsService by splitting TimelineService --- .../android/sdk/api/session/room/Room.kt | 2 + .../session/room/threads/ThreadsService.kt | 68 ++++++++++++ .../session/room/timeline/TimelineService.kt | 42 ------- .../sdk/internal/session/room/DefaultRoom.kt | 3 + .../sdk/internal/session/room/RoomFactory.kt | 3 + .../room/threads/DefaultThreadsService.kt | 103 ++++++++++++++++++ .../room/timeline/DefaultTimelineService.kt | 59 ---------- 7 files changed, 179 insertions(+), 101 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 6c0e730499..d930a5d0fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService @@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.util.Optional */ interface Room : TimelineService, + ThreadsService, SendService, DraftService, ReadService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt new file mode 100644 index 0000000000..34a50a8951 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2022 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.threads + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This interface defines methods to interact with threads related features. It's implemented at the room level within the main timeline. + */ +interface ThreadsService { + + /** + * Get a live list of all the TimelineEvents which have thread replies for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getAllThreadsLive(): LiveData> + + /** + * Get a list of all the TimelineEvents which have thread replies for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getAllThreads(): List + + /** + * Get a live list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotificationsLive(): LiveData> + + /** + * Get a list of all the local unread threads for the specified roomId + * @return the [LiveData] of [TimelineEvent] + */ + fun getNumberOfLocalThreadNotifications(): List + + /** + * Returns whether or not the current user is participating in the thread + * @param rootThreadEventId the eventId of the current thread + */ + fun isUserParticipatingInThread(rootThreadEventId: String): Boolean + + /** + * Enhance the thread list with the edited events if needed + * @return the [LiveData] of [TimelineEvent] + */ + fun mapEventsWithEdition(threads: List): List + + /** + * Marks the current thread as read. This is a local implementation + * @param rootThreadEventId the eventId of the current thread + */ + suspend fun markThreadAsRead(rootThreadEventId: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index c9ccf9ca51..3c021384e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -55,46 +55,4 @@ interface TimelineService { * Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO. */ fun getAttachmentMessages(): List - - /** - * Get a live list of all the TimelineEvents which have thread replies for the specified roomId - * @return the [LiveData] of [TimelineEvent] - */ - fun getAllThreadsLive(): LiveData> - - /** - * Get a list of all the TimelineEvents which have thread replies for the specified roomId - * @return the [LiveData] of [TimelineEvent] - */ - fun getAllThreads(): List - - /** - * Get a live list of all the local unread threads for the specified roomId - * @return the [LiveData] of [TimelineEvent] - */ - fun getNumberOfLocalThreadNotificationsLive(): LiveData> - - /** - * Get a list of all the local unread threads for the specified roomId - * @return the [LiveData] of [TimelineEvent] - */ - fun getNumberOfLocalThreadNotifications(): List - - /** - * Returns whether or not the current user is participating in the thread - * @param rootThreadEventId the eventId of the current thread - */ - fun isUserParticipatingInThread(rootThreadEventId: String): Boolean - - /** - * Enhance the thread list with the edited events if needed - * @return the [LiveData] of [TimelineEvent] - */ - fun mapEventsWithEdition(threads: List): List - - /** - * Marks the current thread as read. This is a local implementation - * @param rootThreadEventId the eventId of the current thread - */ - suspend fun markThreadAsRead(rootThreadEventId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1c3d1971c2..2d8c3e9c78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.tags.TagsService +import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService @@ -54,6 +55,7 @@ import java.security.InvalidParameterException internal class DefaultRoom(override val roomId: String, private val roomSummaryDataSource: RoomSummaryDataSource, private val timelineService: TimelineService, + private val threadsService: ThreadsService, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, @@ -77,6 +79,7 @@ internal class DefaultRoom(override val roomId: String, ) : Room, TimelineService by timelineService, + ThreadsService by threadsService, SendService by sendService, DraftService by draftService, StateService by stateService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 4ab06338a2..70c1ab4f42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.state.DefaultStateService import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService +import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService @@ -50,6 +51,7 @@ internal interface RoomFactory { internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService, private val roomSummaryDataSource: RoomSummaryDataSource, private val timelineServiceFactory: DefaultTimelineService.Factory, + private val threadsServiceFactory: DefaultThreadsService.Factory, private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, @@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomId = roomId, roomSummaryDataSource = roomSummaryDataSource, timelineService = timelineServiceFactory.create(roomId), + threadsService = threadsServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt new file mode 100644 index 0000000000..e6fe5e6a8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2022 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.internal.session.room.threads + +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.realm.Realm +import org.matrix.android.sdk.api.session.room.threads.ThreadsService +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId +import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId +import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread +import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.util.awaitTransaction + +internal class DefaultThreadsService @AssistedInject constructor( + @Assisted private val roomId: String, + @UserId private val userId: String, + @SessionDatabase private val monarchy: Monarchy, + private val timelineEventMapper: TimelineEventMapper, +) : ThreadsService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultThreadsService + } + + override fun getNumberOfLocalThreadNotificationsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getNumberOfLocalThreadNotifications(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getAllThreadsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun getAllThreads(): List { + return monarchy.fetchAllMappedSync( + { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, + { timelineEventMapper.map(it) } + ) + } + + override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { + TimelineEventEntity.isUserParticipatingInThread( + realm = it, + roomId = roomId, + rootThreadEventId = rootThreadEventId, + senderId = userId) + } + } + + override fun mapEventsWithEdition(threads: List): List { + return Realm.getInstance(monarchy.realmConfiguration).use { + threads.mapEventsWithEdition(it, roomId) + } + } + + override suspend fun markThreadAsRead(rootThreadEventId: String) { + monarchy.awaitTransaction { + EventEntity.where( + realm = it, + eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index cc6485698b..748231fc73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -31,16 +31,10 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider -import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId -import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId -import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread -import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where @@ -50,7 +44,6 @@ import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTa import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultTimelineService @AssistedInject constructor( @Assisted private val roomId: String, @@ -119,56 +112,4 @@ internal class DefaultTimelineService @AssistedInject constructor( .orEmpty() } } - - override fun getNumberOfLocalThreadNotificationsLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun getNumberOfLocalThreadNotifications(): List { - return monarchy.fetchAllMappedSync( - { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun getAllThreadsLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun getAllThreads(): List { - return monarchy.fetchAllMappedSync( - { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { timelineEventMapper.map(it) } - ) - } - - override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { - TimelineEventEntity.isUserParticipatingInThread( - realm = it, - roomId = roomId, - rootThreadEventId = rootThreadEventId, - senderId = userId) - } - } - - override fun mapEventsWithEdition(threads: List): List { - return Realm.getInstance(monarchy.realmConfiguration).use { - threads.mapEventsWithEdition(it, roomId) - } - } - - override suspend fun markThreadAsRead(rootThreadEventId: String) { - monarchy.awaitTransaction { - EventEntity.where( - realm = it, - eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE - } - } } From cb3501ea17f5c2c8eac1f824ef8ff3a2a53b6117 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 13:08:15 +0200 Subject: [PATCH 120/130] Lazy load notSupportedThreadsCommands to improve performance --- .../vector/app/features/command/CommandParser.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 7641a22922..3133561ddb 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -64,12 +64,7 @@ class CommandParser @Inject constructor() { val message = textMessage.substring(slashCommand.length).trim() if (isInThreadTimeline) { - val notSupportedCommandsInThreads = Command.values().filter { - !it.isThreadCommand - }.map { - it.command - } - if (notSupportedCommandsInThreads.contains(slashCommand)) { + if (notSupportedThreadsCommands.contains(slashCommand)) { return ParsedCommand.ErrorCommandNotSupportedInThreads(slashCommand) } } @@ -411,6 +406,14 @@ class CommandParser @Inject constructor() { } } + val notSupportedThreadsCommands: List by lazy { + Command.values().filter { + !it.isThreadCommand + }.map { + it.command + } + } + private fun trimParts(message: CharSequence, messageParts: List): String? { val partsSize = messageParts.sumOf { it.length } val gapsNumber = messageParts.size - 1 From d91f3d2de62ce826d43d4f701a08f31f1f9a3d22 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 13:42:20 +0200 Subject: [PATCH 121/130] Enhance SlashCommandNotSupportedInThreads --- .../app/features/command/CommandParser.kt | 28 ++++++++++++++----- .../app/features/command/ParsedCommand.kt | 2 +- .../home/room/detail/TimelineFragment.kt | 2 +- .../composer/MessageComposerViewEvents.kt | 2 +- .../composer/MessageComposerViewModel.kt | 2 +- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 3133561ddb..f0508a2470 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -63,10 +63,9 @@ class CommandParser @Inject constructor() { val slashCommand = messageParts.first() val message = textMessage.substring(slashCommand.length).trim() - if (isInThreadTimeline) { - if (notSupportedThreadsCommands.contains(slashCommand)) { - return ParsedCommand.ErrorCommandNotSupportedInThreads(slashCommand) - } + + getNotSupportedByThreads(isInThreadTimeline, slashCommand)?.let { + return ParsedCommand.ErrorCommandNotSupportedInThreads(it) } when { @@ -406,14 +405,29 @@ class CommandParser @Inject constructor() { } } - val notSupportedThreadsCommands: List by lazy { + private val notSupportedThreadsCommands: List by lazy { Command.values().filter { !it.isThreadCommand - }.map { - it.command } } + /** + * Checks whether or not the current command is not supported by threads + * @param slashCommand the slash command that will be checked + * @param isInThreadTimeline if its true we are in a thread timeline + * @return The command that is not supported + */ + private fun getNotSupportedByThreads(isInThreadTimeline: Boolean, slashCommand: String): Command? { + if (isInThreadTimeline) { + notSupportedThreadsCommands.firstOrNull { + it.command == slashCommand + }?.let { + return it + } + } + return null + } + private fun trimParts(message: CharSequence, messageParts: List): String? { val partsSize = messageParts.sumOf { it.length } val gapsNumber = messageParts.size - 1 diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 590e8786d0..771f721d3c 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -28,7 +28,7 @@ sealed interface ParsedCommand { object ErrorEmptySlashCommand : ParsedCommand - class ErrorCommandNotSupportedInThreads(val slashCommand: String) : ParsedCommand + class ErrorCommandNotSupportedInThreads(val command: Command) : ParsedCommand // Unknown/Unsupported slash command data class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index aa33550b5e..8b8340dd2e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1664,7 +1664,7 @@ class TimelineFragment @Inject constructor( displayCommandError(getString(R.string.not_implemented)) } is MessageComposerViewEvents.SlashCommandNotSupportedInThreads -> { - displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command)) + displayCommandError(getString(R.string.command_not_supported_in_threads, sendMessageResult.command.command)) } } // .exhaustive diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt index 1ae6da6fea..c1af838795 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewEvents.kt @@ -32,7 +32,7 @@ sealed class MessageComposerViewEvents : VectorViewEvents { data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() - class SlashCommandNotSupportedInThreads(val command: String) : SendMessageResult() + class SlashCommandNotSupportedInThreads(val command: Command) : SendMessageResult() data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() object SlashCommandLoading : SendMessageResult() data class SlashCommandResultOk(@StringRes val messageRes: Int? = null) : SendMessageResult() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index b573fd3305..e62a32a7e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -217,7 +217,7 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand)) } is ParsedCommand.ErrorCommandNotSupportedInThreads -> { - _viewEvents.post(MessageComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.slashCommand)) + _viewEvents.post(MessageComposerViewEvents.SlashCommandNotSupportedInThreads(slashCommandResult.command)) } is ParsedCommand.SendPlainText -> { // Send the text message to the room, without markdown From 14e56b8f7d27288d0b58c8486f99b860fd2c6bbb Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 13:53:49 +0200 Subject: [PATCH 122/130] MessageComposerViewModel format --- .../composer/MessageComposerViewModel.kt | 37 +++++++++++-------- .../composer/MessageComposerViewState.kt | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index e62a32a7e0..21b7abe425 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -277,36 +277,42 @@ class MessageComposerViewModel @AssistedInject constructor( handlePartSlashCommand(slashCommandResult) } is ParsedCommand.SendEmote -> { - state.rootThreadEventId?.let { + if (state.rootThreadEventId != null) { room.replyInThread( - rootThreadEventId = it, + rootThreadEventId = state.rootThreadEventId, replyInThreadText = slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) - } ?: room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) + }else{ + room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) + } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendRainbow -> { val message = slashCommandResult.message.toString() - state.rootThreadEventId?.let { + if (state.rootThreadEventId != null) { room.replyInThread( - rootThreadEventId = it, + rootThreadEventId = state.rootThreadEventId, replyInThreadText = slashCommandResult.message, formattedText = rainbowGenerator.generate(message)) - } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message)) + } else { + room.sendFormattedTextMessage(message, rainbowGenerator.generate(message)) + } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) popDraft() } is ParsedCommand.SendRainbowEmote -> { val message = slashCommandResult.message.toString() - state.rootThreadEventId?.let { + if (state.rootThreadEventId != null) { room.replyInThread( - rootThreadEventId = it, + rootThreadEventId = state.rootThreadEventId, replyInThreadText = slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, formattedText = rainbowGenerator.generate(message)) - } ?: room.sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) + } else { + room.sendFormattedTextMessage(message, rainbowGenerator.generate(message), MessageType.MSGTYPE_EMOTE) + } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) popDraft() @@ -314,15 +320,16 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.SendSpoiler -> { val text = "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})" val formattedText = "${slashCommandResult.message}" - state.rootThreadEventId?.let { + if (state.rootThreadEventId != null) { room.replyInThread( - rootThreadEventId = it, + rootThreadEventId = state.rootThreadEventId, replyInThreadText = text, formattedText = formattedText) - } ?: room.sendFormattedTextMessage( - text, - formattedText - ) + } else { + room.sendFormattedTextMessage( + text, + formattedText) + } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) popDraft() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index aae1678cca..f90f3975c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -67,7 +67,7 @@ data class MessageComposerViewState( ) : MavericksState { val isVoiceRecording = when (voiceRecordingUiState) { - VoiceMessageRecorderView.RecordingUiState.Idle -> false + VoiceMessageRecorderView.RecordingUiState.Idle -> false is VoiceMessageRecorderView.RecordingUiState.Locked, VoiceMessageRecorderView.RecordingUiState.Draft, is VoiceMessageRecorderView.RecordingUiState.Recording -> true From 26eaa843b3c45563cf5dc8bd1d25b373802e29ed Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 13:56:17 +0200 Subject: [PATCH 123/130] ktlint format --- .../internal/session/room/timeline/DefaultTimelineService.kt | 1 - .../main/java/im/vector/app/features/command/CommandParser.kt | 1 - .../home/room/detail/composer/MessageComposerViewModel.kt | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 748231fc73..d7d61f0b47 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.realm.Realm import io.realm.Sort import io.realm.kotlin.where import org.matrix.android.sdk.api.MatrixCoroutineDispatchers diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index f0508a2470..81ac0b3c72 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -63,7 +63,6 @@ class CommandParser @Inject constructor() { val slashCommand = messageParts.first() val message = textMessage.substring(slashCommand.length).trim() - getNotSupportedByThreads(isInThreadTimeline, slashCommand)?.let { return ParsedCommand.ErrorCommandNotSupportedInThreads(it) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 21b7abe425..0d90227168 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -283,7 +283,7 @@ class MessageComposerViewModel @AssistedInject constructor( replyInThreadText = slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) - }else{ + } else { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk()) From ec9b6aa9938044beaa861bdf6f81943d9ca186ee Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 14:50:57 +0200 Subject: [PATCH 124/130] Fix error in unit test --- .../java/im/vector/app/features/command/CommandParserTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt index 4af03a36f5..2eb17d91fb 100644 --- a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt +++ b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt @@ -60,7 +60,7 @@ class CommandParserTest { private fun test(message: String, expectedResult: ParsedCommand) { val commandParser = CommandParser() - val result = commandParser.parseSlashCommand(message) + val result = commandParser.parseSlashCommand(message,false) result shouldBeEqualTo expectedResult } } From f07c23fdda521663c865cd012a3ced92a4d1d3e9 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 14:52:09 +0200 Subject: [PATCH 125/130] Formating & remove unused comments --- .../home/room/detail/TimelineFragment.kt | 20 ------------------- .../app/features/command/CommandParserTest.kt | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 8b8340dd2e..ca4c1e51c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1796,7 +1796,6 @@ class TimelineFragment @Inject constructor( } // TimelineEventController.Callback ************************************************************ - override fun onUrlClicked(url: String, title: String): Boolean { viewLifecycleOwner.lifecycleScope.launch { val isManaged = permalinkHandler @@ -1906,28 +1905,12 @@ class TimelineFragment @Inject constructor( } } -// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { -// val isEncrypted = messageFileContent.encryptedFileInfo != null -// val action = RoomDetailAction.DownloadOrOpen(eventId, messageFileContent, isEncrypted) -// // We need WRITE_EXTERNAL permission -// // if (!isEncrypted || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { -// showSnackWithMessage(getString(R.string.downloading_file, messageFileContent.getFileName())) -// roomDetailViewModel.handle(action) -// // } else { -// // roomDetailViewModel.pendingAction = action -// // } -// } - private fun cleanUpAfterPermissionNotGranted() { // Reset all pending data timelineViewModel.pendingAction = null attachmentsHelper.pendingType = null } -// override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { -// vectorBaseActivity.notImplemented("open audio file") -// } - override fun onLoadMore(direction: Timeline.Direction) { timelineViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) } @@ -2377,7 +2360,6 @@ class TimelineFragment @Inject constructor( } // VectorInviteView.Callback - override fun onAcceptInvite() { notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) } timelineViewModel.handle(RoomDetailAction.AcceptInvite) @@ -2398,7 +2380,6 @@ class TimelineFragment @Inject constructor( } // AttachmentTypeSelectorView.Callback - private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { val pendingType = attachmentsHelper.pendingType @@ -2449,7 +2430,6 @@ class TimelineFragment @Inject constructor( } // AttachmentsHelper.Callback - override fun onContentAttachmentsReady(attachments: List) { val grouped = attachments.toGroupedContentAttachmentData() if (grouped.notPreviewables.isNotEmpty()) { diff --git a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt index 2eb17d91fb..f5233a496c 100644 --- a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt +++ b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt @@ -60,7 +60,7 @@ class CommandParserTest { private fun test(message: String, expectedResult: ParsedCommand) { val commandParser = CommandParser() - val result = commandParser.parseSlashCommand(message,false) + val result = commandParser.parseSlashCommand(message, false) result shouldBeEqualTo expectedResult } } From 15fe9edfbd4eac0ed75ab14b2fc4a8a72e89947c Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Mon, 31 Jan 2022 16:21:46 +0200 Subject: [PATCH 126/130] Fix conflicts --- .../vector/app/features/home/room/detail/TimelineFragment.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 18625431e1..afbe641c93 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1930,14 +1930,11 @@ class TimelineFragment @Inject constructor( timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } - else -> { - onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) - } is MessageLocationContent -> { handleShowLocationPreview(messageContent, informationData.senderId) } else -> { - Timber.d("No click action defined for this message content") + onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) } } } From fcc095a239612920c3ff5b41df4022272d74123c Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 1 Feb 2022 12:13:10 +0200 Subject: [PATCH 127/130] PR remarks --- .../org/matrix/android/sdk/flow/FlowRoom.kt | 4 +-- .../session/room/threads/ThreadsService.kt | 29 +++++++++---------- .../database/helper/ThreadEventsHelper.kt | 6 ++-- .../room/threads/DefaultThreadsService.kt | 4 +-- .../app/features/command/CommandParser.kt | 7 ++--- .../home/room/detail/TimelineFragment.kt | 22 +++++++------- .../timeline/TimelineEventController.kt | 2 +- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 1e239069ad..826f584f6a 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -109,9 +109,9 @@ class FlowRoom(private val room: Room) { } fun liveLocalUnreadThreadList(): Flow> { - return room.getNumberOfLocalThreadNotificationsLive().asFlow() + return room.getMarkedThreadNotificationsLive().asFlow() .startWith(room.coroutineDispatchers.io) { - room.getNumberOfLocalThreadNotifications() + room.getMarkedThreadNotifications() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index 34a50a8951..e4d1d979e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -20,33 +20,30 @@ import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** - * This interface defines methods to interact with threads related features. It's implemented at the room level within the main timeline. + * This interface defines methods to interact with threads related features. + * It's implemented at the room level within the main timeline. */ interface ThreadsService { /** - * Get a live list of all the TimelineEvents which have thread replies for the specified roomId - * @return the [LiveData] of [TimelineEvent] + * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level */ fun getAllThreadsLive(): LiveData> /** - * Get a list of all the TimelineEvents which have thread replies for the specified roomId - * @return the [LiveData] of [TimelineEvent] + * Returns a list of all the thread root TimelineEvents that exists at the room level */ fun getAllThreads(): List /** - * Get a live list of all the local unread threads for the specified roomId - * @return the [LiveData] of [TimelineEvent] + * Returns a [LiveData] list of all the marked unread threads that exists at the room level */ - fun getNumberOfLocalThreadNotificationsLive(): LiveData> + fun getMarkedThreadNotificationsLive(): LiveData> /** - * Get a list of all the local unread threads for the specified roomId - * @return the [LiveData] of [TimelineEvent] + * Returns a list of all the marked unread threads that exists at the room level */ - fun getNumberOfLocalThreadNotifications(): List + fun getMarkedThreadNotifications(): List /** * Returns whether or not the current user is participating in the thread @@ -55,14 +52,16 @@ interface ThreadsService { fun isUserParticipatingInThread(rootThreadEventId: String): Boolean /** - * Enhance the thread list with the edited events if needed - * @return the [LiveData] of [TimelineEvent] + * Enhance the provided root thread TimelineEvent [List] by adding the latest + * message edition for that thread + * @return the enhanced [List] with edited updates */ fun mapEventsWithEdition(threads: List): List /** - * Marks the current thread as read. This is a local implementation - * @param rootThreadEventId the eventId of the current thread + * Marks the current thread as read in local DB. + * note: read receipts within threads are not yet supported with the API + * @param rootThreadEventId the root eventId of the current thread */ suspend fun markThreadAsRead(rootThreadEventId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index afc090604a..f703bfaf82 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -159,7 +159,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING) /** - * Map each timelineEvent with the equivalent decrypted text edition/replacement for root threads + * Map each root thread TimelineEvent with the equivalent decrypted text edition/replacement */ internal fun List.mapEventsWithEdition(realm: Realm, roomId: String): List = this.map { @@ -180,8 +180,8 @@ internal fun List.mapEventsWithEdition(realm: Realm, roomId: Stri } /** - * Find the number of all the local notifications for the specified room - * @param roomId The room that the number of notifications will be returned + * Returns a list of all the marked unread threads that exists for the specified room + * @param roomId The roomId that the user is currently in */ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery = TimelineEventEntity diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index e6fe5e6a8b..5967ae8d2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -49,14 +49,14 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getNumberOfLocalThreadNotificationsLive(): LiveData> { + override fun getMarkedThreadNotificationsLive(): LiveData> { return monarchy.findAllMappedWithChanges( { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, { timelineEventMapper.map(it) } ) } - override fun getNumberOfLocalThreadNotifications(): List { + override fun getMarkedThreadNotifications(): List { return monarchy.fetchAllMappedSync( { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, { timelineEventMapper.map(it) } diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 81ac0b3c72..b8bef506b1 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -417,14 +417,13 @@ class CommandParser @Inject constructor() { * @return The command that is not supported */ private fun getNotSupportedByThreads(isInThreadTimeline: Boolean, slashCommand: String): Command? { - if (isInThreadTimeline) { + return if (isInThreadTimeline) { notSupportedThreadsCommands.firstOrNull { it.command == slashCommand - }?.let { - return it } + } else { + null } - return null } private fun trimParts(message: CharSequence, messageParts: List): String? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index afbe641c93..1deed976bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1924,17 +1924,16 @@ class TimelineFragment @Inject constructor( timelineViewModel.handle(action) } is EncryptedEventContent -> { - if (isRootThreadEvent) { - onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) - } else { timelineViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) - } } is MessageLocationContent -> { handleShowLocationPreview(messageContent, informationData.senderId) } else -> { - onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) + val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) + if (!handled) { + Timber.d("No click action defined for this message content") + } } } } @@ -1966,9 +1965,12 @@ class TimelineFragment @Inject constructor( } } - override fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) { - if (vectorPreferences.areThreadMessagesEnabled() && isRootThreadEvent && !isThreadTimeLine()) { + override fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean): Boolean { + return if (vectorPreferences.areThreadMessagesEnabled() && isRootThreadEvent && !isThreadTimeLine()) { navigateToThreadTimeline(eventId) + true + } else { + false } } @@ -2361,7 +2363,7 @@ class TimelineFragment @Inject constructor( } } -// VectorInviteView.Callback + // VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) } timelineViewModel.handle(RoomDetailAction.AcceptInvite) @@ -2381,7 +2383,7 @@ class TimelineFragment @Inject constructor( } } -// AttachmentTypeSelectorView.Callback + // AttachmentTypeSelectorView.Callback private val typeSelectedActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { val pendingType = attachmentsHelper.pendingType @@ -2431,7 +2433,7 @@ class TimelineFragment @Inject constructor( }.exhaustive } -// AttachmentsHelper.Callback + // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { val grouped = attachments.toGroupedContentAttachmentData() if (grouped.notPreviewables.isNotEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 8851f187e0..eaa2ae9453 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -156,7 +156,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface ThreadCallback { - fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) + fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) : Boolean } interface ReadReceiptsCallback { From cfa52d83b4de2d66e07827210b96f94171062c2d Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 1 Feb 2022 12:16:52 +0200 Subject: [PATCH 128/130] ktlint format --- .../home/room/detail/timeline/TimelineEventController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index eaa2ae9453..1efc0377d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -156,7 +156,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface ThreadCallback { - fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean) : Boolean + fun onThreadSummaryClicked(eventId: String, isRootThreadEvent: Boolean): Boolean } interface ReadReceiptsCallback { From 877c9bec979619adc10906abf69e2d0e71c5cc3e Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 1 Feb 2022 14:07:16 +0200 Subject: [PATCH 129/130] Improve hidden events for threads --- .../detail/timeline/factory/MessageItemFactory.kt | 12 ++++++++---- .../detail/timeline/format/NoticeEventFormatter.kt | 5 ++++- .../timeline/helper/TimelineEventVisibilityHelper.kt | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) 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 e57c9740e9..c59a6f5360 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 @@ -84,6 +84,7 @@ import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType +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.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -106,6 +107,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import javax.inject.Inject class MessageItemFactory @Inject constructor( @@ -125,6 +127,7 @@ class MessageItemFactory @Inject constructor( private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val lightweightSettingsStorage: LightweightSettingsStorage, private val spanUtils: SpanUtils, private val session: Session, private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, @@ -168,6 +171,11 @@ class MessageItemFactory @Inject constructor( return noticeItemFactory.create(params) } + // This is a thread event and we will [debug] display it when we are in the main timeline + if (lightweightSettingsStorage.areThreadMessagesEnabled() && !params.isFromThreadTimeline() && event.root.isThread()) { + return noticeItemFactory.create(params) + } + // always hide summary when we are on thread timeline val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails) @@ -200,10 +208,6 @@ class MessageItemFactory @Inject constructor( } } - private fun isFromThreadTimeline(params: TimelineItemFactoryParams) { - params.rootThreadEventId - } - private fun buildLocationItem(locationContent: MessageLocationContent, informationData: MessageInformationData, highlight: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index ae541217bf..c7be395693 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse 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.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.Membership @@ -104,6 +105,7 @@ class NoticeEventFormatter @Inject constructor( EventType.STATE_SPACE_CHILD, EventType.STATE_SPACE_PARENT, EventType.REDACTION, + EventType.STICKER, EventType.POLL_RESPONSE, EventType.POLL_END -> formatDebug(timelineEvent.root) else -> { @@ -194,7 +196,8 @@ class NoticeEventFormatter @Inject constructor( } private fun formatDebug(event: Event): CharSequence { - return "Debug: event type \"${event.getClearType()}\"" + val threadPrefix = if (event.isThread()) "thread" else "" + return "Debug: $threadPrefix event type \"${event.getClearType()}\"" } private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 7656a671f8..f317eb4f9a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -117,7 +117,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen rootThreadEventId: String? ): Boolean { // If show hidden events is true we should always display something - if (userPreferencesProvider.shouldShowHiddenEvents()) { + if (userPreferencesProvider.shouldShowHiddenEvents() && !isFromThreadTimeline) { return true } // We always show highlighted event From ed992ddc72b4ea29c67d9cf7bdf950c603806d82 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 1 Feb 2022 14:40:00 +0200 Subject: [PATCH 130/130] Formatting --- .../home/room/detail/timeline/factory/MessageItemFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c59a6f5360..59b7ba3a8c 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 @@ -171,8 +171,8 @@ class MessageItemFactory @Inject constructor( return noticeItemFactory.create(params) } - // This is a thread event and we will [debug] display it when we are in the main timeline if (lightweightSettingsStorage.areThreadMessagesEnabled() && !params.isFromThreadTimeline() && event.root.isThread()) { + // This is a thread event and we will [debug] display it when we are in the main timeline return noticeItemFactory.create(params) }