diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt new file mode 100644 index 0000000000..2339287cbe --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt @@ -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(), IsEventItem { + + @EpoxyAttribute lateinit var eventId: String + + override fun getEventIds(): List { + return listOf(eventId) + } + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt index af56e2eb02..dbe4c484ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 29871cf307..9acd34c827 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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>) = synchronized(modelCache) { - positionOfReadMarker = null - adapterPositionMapping.clear() - val callIds = mutableSetOf() - 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] } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 982ceb906c..837d35d15f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) + } + } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt new file mode 100644 index 0000000000..30b11b5e2c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -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, + private val adapterPositionMapping: MutableMap, + private val vectorPreferences: VectorPreferences, + private val callManager: WebRtcCallManager +) { + + private var previousModelsSize = 0 + + // Update position when we are building new items + fun intercept( + models: MutableList>, + unreadState: UnreadState, + timeline: Timeline?, + callback: TimelineEventController.Callback? + ) { + positionOfReadMarker.set(null) + adapterPositionMapping.clear() + val callIds = mutableSetOf() + + // 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>.addReadMarkerItem(callback: TimelineEventController.Callback?) { + val readMarker = TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + add(readMarker) + } + + private fun MutableListIterator>.removeCallItemIfNeeded( + epoxyModel: CallTileTimelineItem, + callIds: MutableSet, + 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>.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>.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) + } + } + } + +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index e617489902..8546ed7e0f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter /** * Children must override getViewType() */ -abstract class BaseEventItem : VectorEpoxyModel() { +abstract class BaseEventItem : VectorEpoxyModel(), IsEventItem { // To use for instance when opening a permalink with an eventId @EpoxyAttribute @@ -53,12 +53,6 @@ abstract class BaseEventItem : VectorEpoxyModel 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 - abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/IsEventItem.kt similarity index 65% rename from vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/IsEventItem.kt index aaf870667b..059bdbea43 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/IsEventItem.kt @@ -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() { - 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 } diff --git a/vector/src/main/res/layout/item_empty.xml b/vector/src/main/res/layout/item_timeline_empty.xml similarity index 100% rename from vector/src/main/res/layout/item_empty.xml rename to vector/src/main/res/layout/item_timeline_empty.xml