TimelineControllerInterceptorHelper.kt 7.66 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
 * 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
ganfra's avatar
ganfra committed
20
21
22
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem_
23
import im.vector.app.core.resources.UserPreferencesProvider
24
25
26
27
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.CallTileTimelineItem
28
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
Benoit Marty's avatar
Benoit Marty committed
29
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
30
31
32
33
34
35
36
37
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0

private const val DEFAULT_PREFETCH_THRESHOLD = 30

class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
                                          private val adapterPositionMapping: MutableMap<String, Int>,
38
                                          private val userPreferencesProvider: UserPreferencesProvider,
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
                                          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()
60
        val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
61
62
        var index = 0
        val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
63
64
        var atLeastOneVisibleItemSinceLastDaySeparator = false
        var atLeastOneVisibleItemsBeforeReadMarker = false
65
        var appendReadMarker = false
66

67
68
        // Then iterate on models so we have the exact positions in the adapter
        modelsIterator.forEach { epoxyModel ->
Benoit Marty's avatar
Benoit Marty committed
69
            if (epoxyModel is ItemWithEvents) {
70
71
72
73
                if (epoxyModel.isVisible()) {
                    atLeastOneVisibleItemSinceLastDaySeparator = true
                    atLeastOneVisibleItemsBeforeReadMarker = true
                }
74
75
                epoxyModel.getEventIds().forEach { eventId ->
                    adapterPositionMapping[eventId] = index
76
                    appendReadMarker = epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker
77
78
                }
            }
79
80
            if (epoxyModel is DaySeparatorItem) {
                if (!atLeastOneVisibleItemSinceLastDaySeparator) {
81
82
83
84
                    modelsIterator.remove()
                    return@forEach
                }
                atLeastOneVisibleItemSinceLastDaySeparator = false
85
            } else if (epoxyModel is CallTileTimelineItem) {
86
                val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
87
                if (!hasBeenRemoved) {
88
89
                    atLeastOneVisibleItemSinceLastDaySeparator = true
                }
90
            }
91
92
93
94
95
96
            if (appendReadMarker) {
                modelsIterator.addReadMarkerItem(callback)
                index++
                positionOfReadMarker.set(index)
                appendReadMarker = false
            }
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
            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
115
    ): Boolean {
116
117
118
119
120
        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())
121
122
        val removed = shouldRemoveCallItem && !showHiddenEvents
        if (removed) {
123
124
125
126
            remove()
            val emptyItem = TimelineEmptyItem_()
                    .id(epoxyModel.id())
                    .eventId(epoxyModel.attributes.informationData.eventId)
127
                    .notBlank(false)
128
129
130
            add(emptyItem)
        }
        callIds.add(callId)
131
        return removed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    }

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

ganfra's avatar
ganfra committed
150
    private fun MutableList<EpoxyModel<*>>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
        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)
            }
        }
    }
}