Handle chunks merging with thread summary

Add animation to fragment transition with offset for recyclerview initialization
Support threads on deleted events
This commit is contained in:
ariskotsomitopoulos 2021-11-25 17:59:28 +02:00
parent afc69c77bd
commit c4967a2871
17 changed files with 80 additions and 110 deletions

View file

@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.extensions.assertIsManaged
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import timber.log.Timber
@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
this.senderName = timelineEventEntity.senderName
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
}
handleThreadSummary(realm, eventId, copied)
timelineEvents.add(copied)
}
/**
* Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one
*/
private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
EventEntity
.whereRoomId(realm, newTimelineEventEntity.roomId)
.equalTo(EventEntityFields.IS_ROOT_THREAD, true)
.equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
.findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
}
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {

View file

@ -49,6 +49,13 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId)
}
internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOM_ID, roomId)
}
internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())

View file

@ -270,53 +270,4 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
}
// /**
// * Mark or update the thread root event accordingly. If the Threading is disabled
// * no action is done
// */
// private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) {
//
// if (!BuildConfig.THREADING_ENABLED) return
//
// val rootThreadEventId = eventEntity.rootThreadEventId
//
// if (eventEntity.isThread && rootThreadEventId != null) {
// markEventAsRootEvent(realm, rootThreadEventId)
// } else {
// markAsRootEventIfNeeded(realm, eventEntity.eventId)
// }
// }
// /**
// * Finds the event with rootThreadEventId and marks it as a root thread
// */
// private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
// val rootThreadEvent = EventEntity
// .where(realm, rootThreadEventId)
// .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
// rootThreadEvent.isThread = true
// }
//
// /**
// * Also check if there is at least one thread message for that rootThreadEventId,
// * that means it is a root thread so it should be updated accordingly
// */
// private fun markAsRootEventIfNeeded(realm: Realm, candidateIdRootThread: String) {
// EventEntity
// .whereRootThreadEventId(realm, candidateIdRootThread)
// .findFirst() ?: return
//
// markEventAsRootEvent(realm, candidateIdRootThread)
// }
// /**
// * Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
// */
// private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
// return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
// ?: realm.createObject<ChunkEntity>().apply {
// this.rootThreadEventId = rootThreadEventId
// }
// }
}

View file

@ -125,19 +125,19 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId)
}
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
val callback = params.callback
event.root.eventId ?: return null
roomId = event.roomId
val informationData = messageInformationDataFactory.create(params)
val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails
if (event.root.isRedacted()) {
// message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val attributes = messageItemAttributesFactory.create(null, informationData, callback, threadDetails)
return buildRedactedItem(attributes, highlight)
}
@ -154,7 +154,6 @@ class MessageItemFactory @Inject constructor(
}
// always hide summary when we are on thread timeline
val threadDetails = if(params.isFromThreadTimeline()) null else event.root.threadDetails
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails)
// val all = event.root.toContent()
@ -183,6 +182,7 @@ class MessageItemFactory @Inject constructor(
private fun isFromThreadTimeline(params: TimelineItemFactoryParams) {
params.rootThreadEventId
}
private fun buildOptionsMessageItem(messageContent: MessageOptionsContent,
informationData: MessageInformationData,
highlight: Boolean,

View file

@ -72,6 +72,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
}
}
override fun bind(holder: H) {
super.bind(holder)
if (attributes.informationData.showInformation) {
@ -117,20 +118,22 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
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)
}
val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let
val displayName = threadDetails.threadSummarySenderInfo?.displayName
val avatarUrl = threadDetails.threadSummarySenderInfo?.avatarUrl
attributes.avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView)
} ?: run { holder.threadSummaryConstraintLayout.isVisible = false }
}
}
override fun unbind(holder: H) {
attributes.avatarRenderer.clear(holder.avatarImageView)
holder.avatarImageView.setOnClickListener(null)
holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnClickListener(null)
holder.memberNameView.setOnLongClickListener(null)
attributes.avatarRenderer.clear(holder.threadSummaryAvatarImageView)
holder.threadSummaryConstraintLayout.setOnClickListener(null)
super.unbind(holder)
}

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home.room.threads.list.model
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
@ -31,8 +30,8 @@ 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<ThreadSummaryModel.Holder>() {
@EpoxyModelClass(layout = R.layout.item_thread_list)
abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem

View file

@ -20,21 +20,21 @@ import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.threads.list.model.threadSummary
import im.vector.app.features.home.room.threads.list.model.threadList
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class ThreadSummaryController @Inject constructor(
class ThreadListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val dateFormatter: VectorDateFormatter
) : EpoxyController() {
var listener: Listener? = null
private var viewState: ThreadSummaryViewState? = null
private var viewState: ThreadListViewState? = null
fun update(viewState: ThreadSummaryViewState) {
fun update(viewState: ThreadListViewState) {
this.viewState = viewState
requestModelBuild()
}
@ -46,7 +46,7 @@ class ThreadSummaryController @Inject constructor(
safeViewState.rootThreadEventList.invoke()
?.forEach { timelineEvent ->
val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
threadSummary {
threadList {
id(timelineEvent.eventId)
avatarRenderer(host.avatarRenderer)
matrixItem(timelineEvent.senderInfo.toMatrixItem())

View file

@ -31,23 +31,23 @@ import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.flow.flow
class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState,
class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState,
private val session: Session) :
VectorViewModel<ThreadSummaryViewState, EmptyAction, EmptyViewEvents>(initialState) {
VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)
@AssistedFactory
interface Factory {
fun create(initialState: ThreadSummaryViewState): ThreadSummaryViewModel
fun create(initialState: ThreadListViewState): ThreadListViewModel
}
companion object : MavericksViewModelFactory<ThreadSummaryViewModel, ThreadSummaryViewState> {
companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ThreadSummaryViewState): ThreadSummaryViewModel? {
override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? {
val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.threadSummaryViewModelFactory.create(state)
return fragment.threadListViewModelFactory.create(state)
}
}

View file

@ -22,7 +22,7 @@ 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(
data class ThreadListViewState(
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false,
val roomId: String

View file

@ -20,14 +20,13 @@ 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
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThreadListBinding>() {
@ -35,8 +34,7 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThr
return BottomSheetThreadListBinding.inflate(inflater, container, false)
}
private val threadListViewModel: ThreadSummaryViewModel by parentFragmentViewModel()
private val threadListViewModel: ThreadListViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -52,18 +50,11 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThr
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
}
private fun renderState(state: ThreadListViewState) {
val tickDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick)
views.threadListModalAllThreads.rightIcon = if (state.shouldFilterThreads) null else tickDrawable
views.threadListModalMyThreads.rightIcon = if (state.shouldFilterThreads) tickDrawable else null
}
}

View file

@ -34,20 +34,20 @@ import im.vector.app.features.home.AvatarRenderer
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.list.viewmodel.ThreadSummaryController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
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 avatarRenderer: AvatarRenderer,
private val threadSummaryController: ThreadSummaryController,
val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory
private val threadListController: ThreadListController,
val threadListViewModelFactory: ThreadListViewModel.Factory
) : VectorBaseFragment<FragmentThreadListBinding>(),
ThreadSummaryController.Listener {
ThreadListController.Listener {
private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel()
private val threadListViewModel: ThreadListViewModel by fragmentViewModel()
private val threadListArgs: ThreadListArgs by args()
@ -74,13 +74,13 @@ class ThreadListFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initToolbar()
views.threadListRecyclerView.configureWith(threadSummaryController, TimelineItemAnimator(), hasFixedSize = false)
threadSummaryController.listener = this
views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false)
threadListController.listener = this
}
override fun onDestroyView() {
views.threadListRecyclerView.cleanup()
threadSummaryController.listener = null
threadListController.listener = null
super.onDestroyView()
}
@ -89,8 +89,8 @@ class ThreadListFragment @Inject constructor(
renderToolbar()
}
override fun invalidate() = withState(threadSummaryViewModel) { state ->
threadSummaryController.update(state)
override fun invalidate() = withState(threadListViewModel) { state ->
threadListController.update(state)
}
private fun renderToolbar() {

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0"
<translate
android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
<translate
android:startOffset="250"
android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
<translate
android:startOffset="250"
android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p"
<translate
android:fromXDelta="0" android:toXDelta="100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>

View file

@ -34,7 +34,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="?android:colorBackground"
tools:listitem="@layout/item_thread_summary" />
tools:listitem="@layout/item_thread_list" />
</androidx.constraintlayout.widget.ConstraintLayout>