From afc69c77bda302c845014bdddd0c5ce78191706f Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 24 Nov 2021 18:23:33 +0200 Subject: [PATCH] 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