mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Add local filtering in thread list
This commit is contained in:
parent
e2bf3e7097
commit
afc69c77bd
12 changed files with 199 additions and 46 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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> =
|
||||
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
|
||||
|
|
|
@ -111,7 +111,7 @@ internal object EventMapper {
|
|||
avatarUrl = timelineEventEntity.senderAvatar
|
||||
)
|
||||
},
|
||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedUserFriendlyTextSummary().orEmpty()
|
||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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<ThreadSummaryViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
VectorViewModel<ThreadSummaryViewState, EmptyAction, EmptyViewEvents>(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
47
vector/src/main/res/layout/bottom_sheet_thread_list.xml
Normal file
47
vector/src/main/res/layout/bottom_sheet_thread_list.xml
Normal 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>
|
|
@ -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 you’ve participated in</string>
|
||||
|
||||
<!-- Room events -->
|
||||
<string name="room_event_action_report_prompt_reason">Reason for reporting this content</string>
|
||||
|
|
Loading…
Reference in a new issue