Add local filtering in thread list

This commit is contained in:
ariskotsomitopoulos 2021-11-24 18:23:33 +02:00
parent e2bf3e7097
commit afc69c77bd
12 changed files with 199 additions and 46 deletions

View File

@ -195,7 +195,7 @@ data class Event(
* It can be used especially for message summaries. * It can be used especially for message summaries.
* It will return a decrypted text message or an empty string otherwise. * It will return a decrypted text message or an empty string otherwise.
*/ */
fun getDecryptedUserFriendlyTextSummary(): String { fun getDecryptedTextSummary(): String {
val text = getDecryptedValue().orEmpty() val text = getDecryptedValue().orEmpty()
return when { return when {
isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)

View File

@ -68,4 +68,11 @@ interface TimelineService {
*/ */
fun getAllThreads(): List<TimelineEvent> fun getAllThreads(): List<TimelineEvent>
/**
* 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
} }

View File

@ -86,7 +86,6 @@ internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEv
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll() .findAll()
/** /**
* Find all TimelineEventEntity that are root threads for the specified room * Find all TimelineEventEntity that are root threads for the specified room
* @param roomId The room that all stored root threads will be returned * @param roomId The room that all stored root threads will be returned
@ -97,3 +96,17 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true) .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .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

View File

@ -111,7 +111,7 @@ internal object EventMapper {
avatarUrl = timelineEventEntity.senderAvatar avatarUrl = timelineEventEntity.senderAvatar
) )
}, },
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedUserFriendlyTextSummary().orEmpty() threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
) )
} }
} }

View File

@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.isImageMessage 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.api.util.Optional
import org.matrix.android.sdk.internal.database.RealmSessionProvider 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.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.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity 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.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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase 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.room.membership.LoadRoomMembersTask
@ -111,10 +112,21 @@ internal class DefaultTimelineService @AssistedInject constructor(
{ timelineEventMapper.map(it) } { timelineEventMapper.map(it) }
) )
} }
override fun getAllThreads(): List<TimelineEvent> { override fun getAllThreads(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) } { 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)
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 New Vector Ltd * Copyright 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 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) { fun update(viewState: ThreadSummaryViewState) {
this.viewState = viewState this.viewState = viewState
requestModelBuild() requestModelBuild()
@ -48,13 +42,7 @@ class ThreadSummaryController @Inject constructor(
override fun buildModels() { override fun buildModels() {
val safeViewState = viewState ?: return val safeViewState = viewState ?: return
val host = this 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() safeViewState.rootThreadEventList.invoke()
?.forEach { timelineEvent -> ?.forEach { timelineEvent ->
val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST) val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@ -64,7 +52,7 @@ class ThreadSummaryController @Inject constructor(
matrixItem(timelineEvent.senderInfo.toMatrixItem()) matrixItem(timelineEvent.senderInfo.toMatrixItem())
title(timelineEvent.senderInfo.displayName) title(timelineEvent.senderInfo.displayName)
date(date) date(date)
rootMessage(timelineEvent.root.getDecryptedUserFriendlyTextSummary()) rootMessage(timelineEvent.root.getDecryptedTextSummary())
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty()) lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 New Vector Ltd * Copyright 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,11 +26,9 @@ import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment 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.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 import org.matrix.android.sdk.flow.flow
class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState, class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState,
@ -54,19 +52,28 @@ class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialSt
} }
init { init {
observeThreadsSummary() observeThreadsList(initialState.shouldFilterThreads)
} }
override fun handle(action: EmptyAction) { override fun handle(action: EmptyAction) {}
// No op
}
private fun observeThreadsList(shouldFilterThreads: Boolean) =
private fun observeThreadsSummary() {
room?.flow() room?.flow()
?.liveThreadList() ?.liveThreadList()
?.map {
if (!shouldFilterThreads) return@map it
it.filter { timelineEvent ->
room.isUserParticipatingInThread(timelineEvent.eventId, session.myUserId)
}
}
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads -> ?.execute { asyncThreads ->
copy(rootThreadEventList = asyncThreads) copy(
} rootThreadEventList = asyncThreads,
shouldFilterThreads = shouldFilterThreads)
}
fun applyFiltering(shouldFilterThreads: Boolean) {
observeThreadsList(shouldFilterThreads)
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 New Vector Ltd * Copyright 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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( data class ThreadSummaryViewState(
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized, val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false,
val roomId: String val roomId: String
) : MavericksState{ ) : MavericksState{

View File

@ -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<BottomSheetThreadListBinding>() {
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
}
}
}

View File

@ -18,11 +18,10 @@ package im.vector.app.features.home.room.threads.list.views
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState 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.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentThreadListBinding import im.vector.app.databinding.FragmentThreadListBinding
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsAnimator import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator
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.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs 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.ThreadSummaryController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel 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.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
class ThreadListFragment @Inject constructor( class ThreadListFragment @Inject constructor(
private val session: Session,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val threadSummaryController: ThreadSummaryController, private val threadSummaryController: ThreadSummaryController,
val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory
@ -67,10 +61,20 @@ class ThreadListFragment @Inject constructor(
super.onCreate(savedInstanceState) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initToolbar() initToolbar()
views.threadListRecyclerView.configureWith(threadSummaryController, BreadcrumbsAnimator(), hasFixedSize = false) views.threadListRecyclerView.configureWith(threadSummaryController, TimelineItemAnimator(), hasFixedSize = false)
threadSummaryController.listener = this threadSummaryController.listener = this
} }

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?colorSurface"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/threadListModalTitle"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="22dp"
android:text="@string/thread_list_modal_title"
android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/threadListModalAllThreads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:actionDescription="@string/thread_list_modal_all_threads_subtitle"
app:actionTitle="@string/thread_list_modal_all_threads_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/threadListModalTitle"
app:tint="?vctr_content_primary"
app:titleTextColor="?vctr_content_primary" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/threadListModalMyThreads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:actionDescription="@string/thread_list_modal_my_threads_subtitle"
app:actionTitle="@string/thread_list_modal_my_threads_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/threadListModalAllThreads"
app:tint="?vctr_content_primary"
app:titleTextColor="?vctr_content_primary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1027,10 +1027,15 @@
<string name="room_details_people_invited_group_name">INVITED</string> <string name="room_details_people_invited_group_name">INVITED</string>
<string name="room_details_people_present_group_name">JOINED</string> <string name="room_details_people_present_group_name">JOINED</string>
<!-- Room Threads --> <!-- Threads -->
<string name="room_threads_filter">Filter Threads in room</string> <string name="room_threads_filter">Filter Threads in room</string>
<string name="thread_timeline_title">Thread</string> <string name="thread_timeline_title">Thread</string>
<string name="thread_list_title">Threads</string> <string name="thread_list_title">Threads</string>
<string name="thread_list_modal_title">Filter</string>
<string name="thread_list_modal_all_threads_title">All Threads</string>
<string name="thread_list_modal_all_threads_subtitle">Shows all threads from current room</string>
<string name="thread_list_modal_my_threads_title">My Threads</string>
<string name="thread_list_modal_my_threads_subtitle">Shows all threads youve participated in</string>
<!-- Room events --> <!-- Room events -->
<string name="room_event_action_report_prompt_reason">Reason for reporting this content</string> <string name="room_event_action_report_prompt_reason">Reason for reporting this content</string>