From 722f367690b0d7268397efaf6f4c05cfbe97180b Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Tue, 23 Nov 2021 13:34:24 +0200 Subject: [PATCH] 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