mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 09:56:00 +03:00
Timeline: start refactoring the Interceptor mechanism
This commit is contained in:
parent
918d820a5c
commit
61373b8b51
8 changed files with 226 additions and 93 deletions
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2019 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.core.epoxy
|
||||
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.timeline.item.IsEventItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_empty)
|
||||
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), IsEventItem {
|
||||
|
||||
@EpoxyAttribute lateinit var eventId: String
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
return listOf(eventId)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
|
@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import im.vector.app.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.IsEventItem
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
@ -47,7 +48,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
|||
if (layoutManager.findFirstVisibleItemPosition() != position) {
|
||||
return
|
||||
}
|
||||
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
|
||||
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? IsEventItem ?: return
|
||||
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull()
|
||||
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
|
||||
if (indexOfFirstNewItem != -1) {
|
||||
|
|
|
@ -38,18 +38,15 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
|
|||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ReadMarkerVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
|
@ -194,75 +191,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
}
|
||||
}
|
||||
|
||||
private val interceptorHelper = TimelineControllerInterceptorHelper(
|
||||
::positionOfReadMarker,
|
||||
adapterPositionMapping,
|
||||
vectorPreferences,
|
||||
callManager
|
||||
)
|
||||
|
||||
init {
|
||||
addInterceptor(this)
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
// Update position when we are building new items
|
||||
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||
positionOfReadMarker = null
|
||||
adapterPositionMapping.clear()
|
||||
val callIds = mutableSetOf<String>()
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
|
||||
modelsIterator.withIndex().forEach {
|
||||
val index = it.index
|
||||
val epoxyModel = it.value
|
||||
if (epoxyModel is CallTileTimelineItem) {
|
||||
val callId = epoxyModel.attributes.callId
|
||||
// We should remove the call tile if we already have one for this call or
|
||||
// if this is an active call tile without an actual call (which can happen with permalink)
|
||||
val shouldRemoveCallItem = callIds.contains(callId)
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
|
||||
if (shouldRemoveCallItem && !showHiddenEvents) {
|
||||
modelsIterator.remove()
|
||||
return@forEach
|
||||
}
|
||||
callIds.add(callId)
|
||||
}
|
||||
if (epoxyModel is BaseEventItem) {
|
||||
epoxyModel.getEventIds().forEach { eventId ->
|
||||
adapterPositionMapping[eventId] = index
|
||||
}
|
||||
}
|
||||
}
|
||||
val currentUnreadState = this.unreadState
|
||||
if (currentUnreadState is UnreadState.HasUnread) {
|
||||
val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
|
||||
positionOfReadMarker = position
|
||||
if (position != null) {
|
||||
val readMarker = TimelineReadMarkerItem_()
|
||||
.also {
|
||||
it.id("read_marker")
|
||||
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||
}
|
||||
models.add(position, readMarker)
|
||||
}
|
||||
}
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
|
||||
models.add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
models.add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
interceptorHelper.intercept(models, unreadState, timeline, callback)
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
|
@ -431,6 +373,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
}
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
|
||||
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
|
||||
return
|
||||
|
@ -461,14 +411,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
return shouldAdd
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) {
|
||||
return adapterPositionMapping[eventId]
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.epoxy.EmptyItem_
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
|
@ -114,6 +115,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
Timber.e(throwable, "failed to create message item")
|
||||
defaultItemFactory.create(event, highlight, callback, throwable)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
return (computedModel ?: buildEmptyItem(event))
|
||||
}
|
||||
|
||||
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem{
|
||||
return TimelineEmptyItem_()
|
||||
.id(timelineEvent.localId)
|
||||
.eventId(timelineEvent.eventId)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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.detail.timeline.helper
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.IsEventItem
|
||||
import timber.log.Timber
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
|
||||
private val adapterPositionMapping: MutableMap<String, Int>,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
private var previousModelsSize = 0
|
||||
|
||||
// Update position when we are building new items
|
||||
fun intercept(
|
||||
models: MutableList<EpoxyModel<*>>,
|
||||
unreadState: UnreadState,
|
||||
timeline: Timeline?,
|
||||
callback: TimelineEventController.Callback?
|
||||
) {
|
||||
positionOfReadMarker.set(null)
|
||||
adapterPositionMapping.clear()
|
||||
val callIds = mutableSetOf<String>()
|
||||
|
||||
// Add some prefetch loader if needed
|
||||
models.addBackwardPrefetchIfNeeded(timeline, callback)
|
||||
models.addForwardPrefetchIfNeeded(timeline, callback)
|
||||
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
|
||||
var index = 0
|
||||
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
|
||||
// Then iterate on models so we have the exact positions in the adapter
|
||||
modelsIterator.forEach { epoxyModel ->
|
||||
Timber.v("Index of model:${epoxyModel::class}: $index")
|
||||
if (epoxyModel is IsEventItem) {
|
||||
epoxyModel.getEventIds().forEach { eventId ->
|
||||
adapterPositionMapping[eventId] = index
|
||||
if (eventId == firstUnreadEventId) {
|
||||
modelsIterator.addReadMarkerItem(callback)
|
||||
index++
|
||||
positionOfReadMarker.set(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (epoxyModel is CallTileTimelineItem) {
|
||||
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
|
||||
}
|
||||
index++
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
}
|
||||
|
||||
private fun MutableListIterator<EpoxyModel<*>>.addReadMarkerItem(callback: TimelineEventController.Callback?) {
|
||||
val readMarker = TimelineReadMarkerItem_()
|
||||
.also {
|
||||
it.id("read_marker")
|
||||
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||
}
|
||||
add(readMarker)
|
||||
}
|
||||
|
||||
private fun MutableListIterator<EpoxyModel<*>>.removeCallItemIfNeeded(
|
||||
epoxyModel: CallTileTimelineItem,
|
||||
callIds: MutableSet<String>,
|
||||
showHiddenEvents: Boolean
|
||||
) {
|
||||
val callId = epoxyModel.attributes.callId
|
||||
// We should remove the call tile if we already have one for this call or
|
||||
// if this is an active call tile without an actual call (which can happen with permalink)
|
||||
val shouldRemoveCallItem = callIds.contains(callId)
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
|
||||
if (shouldRemoveCallItem && !showHiddenEvents) {
|
||||
remove()
|
||||
val emptyItem = TimelineEmptyItem_()
|
||||
.id(epoxyModel.id())
|
||||
.eventId(epoxyModel.attributes.informationData.eventId)
|
||||
add(emptyItem)
|
||||
}
|
||||
callIds.add(callId)
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback)
|
||||
|
||||
add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addForwardPrefetchIfNeeded(timeline: Timeline?,callback: TimelineEventController.Callback?) {
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback)
|
||||
add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(
|
||||
direction: Timeline.Direction,
|
||||
callback: TimelineEventController.Callback?
|
||||
): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||
/**
|
||||
* Children must override getViewType()
|
||||
*/
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>(), IsEventItem {
|
||||
|
||||
// To use for instance when opening a permalink with an eventId
|
||||
@EpoxyAttribute
|
||||
|
@ -53,12 +53,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
holder.checkableBackground.isChecked = highlighted
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the eventIds associated with the EventItem.
|
||||
* Will generally get only one, but it handles the merging items.
|
||||
*/
|
||||
abstract fun getEventIds(): List<String>
|
||||
|
||||
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
|
||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
|
@ -14,12 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.epoxy
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_empty)
|
||||
abstract class EmptyItem : VectorEpoxyModel<EmptyItem.Holder>() {
|
||||
class Holder : VectorEpoxyHolder()
|
||||
interface IsEventItem {
|
||||
/**
|
||||
* Returns the eventIds associated with the EventItem.
|
||||
* Will generally get only one, but it handles the merging items.
|
||||
*/
|
||||
fun getEventIds(): List<String>
|
||||
}
|
Loading…
Reference in a new issue