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 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)

View File

@ -68,4 +68,11 @@ interface TimelineService {
*/
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)
.findAll()
/**
* Find all TimelineEventEntity that are root threads for the specified room
* @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)
.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
)
},
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.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<TimelineEvent> {
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)
}
}
}

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");
* 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())

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");
* 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.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,
@ -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 observeThreadsSummary() {
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)
}
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");
* 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<List<TimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false,
val roomId: String
) : 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.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
}

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_present_group_name">JOINED</string>
<!-- Room Threads -->
<!-- Threads -->
<string name="room_threads_filter">Filter Threads in room</string>
<string name="thread_timeline_title">Thread</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 -->
<string name="room_event_action_report_prompt_reason">Reason for reporting this content</string>