diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f14df5ada2..dd11b22b64 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -42,10 +42,11 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList import kotlin.collections.HashMap +import kotlin.math.max +import kotlin.math.min private const val MIN_FETCHING_COUNT = 30 -private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE internal class DefaultTimeline( private val roomId: String, @@ -85,8 +86,8 @@ internal class DefaultTimeline( private var roomEntity: RoomEntity? = null - private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN - private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN + private var prevDisplayIndex: Int? = null + private var nextDisplayIndex: Int? = null private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) @@ -222,6 +223,7 @@ internal class DefaultTimeline( if (isStarted.compareAndSet(true, false)) { eventDecryptor.destroy() Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.post { cancelableBag.cancel() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() @@ -303,11 +305,8 @@ internal class DefaultTimeline( private fun hasMoreInCache(direction: Timeline.Direction): Boolean { return Realm.getInstance(realmConfiguration).use { localRealm -> val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) - ?: return false + ?: return false if (direction == Timeline.Direction.FORWARDS) { - if (findCurrentChunk(localRealm)?.isLastForward == true) { - return false - } val firstEvent = builtEvents.firstOrNull() ?: return true firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex } else { @@ -334,16 +333,17 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results * @return true if createSnapshot should be posted */ - private fun paginateInternal(startDisplayIndex: Int, + private fun paginateInternal(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Int): Boolean { + count: Int, + strict: Boolean = false): Boolean { updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } - val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) + val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) if (shouldFetchMore) { val newRequestedCount = count - builtCount updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } - val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) + val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) executePaginationTask(direction, fetchingCount) } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } @@ -404,20 +404,19 @@ internal class DefaultTimeline( .findFirst() shouldFetchInitialEvent = initialEvent == null initialEvent?.root?.displayIndex - } ?: DISPLAY_INDEX_UNKNOWN - + } prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex val currentInitialEventId = initialEventId if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = Math.min(settings.initialSize, liveEvents.size) + val count = min(settings.initialSize, liveEvents.size) if (isLive) { - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) } else { - paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2) - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true) } } postSnapshot() @@ -429,9 +428,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -479,14 +478,15 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it access realm live results * @return number of items who have been added */ - private fun buildTimelineEvents(startDisplayIndex: Int, + private fun buildTimelineEvents(startDisplayIndex: Int?, direction: Timeline.Direction, - count: Long): Int { - if (count < 1) { + count: Long, + strict: Boolean = false): Int { + if (count < 1 || startDisplayIndex == null) { return 0 } val start = System.currentTimeMillis() - val offsetResults = getOffsetResults(startDisplayIndex, direction, count) + val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict) if (offsetResults.isEmpty()) { return 0 } @@ -501,7 +501,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } @@ -527,16 +527,23 @@ internal class DefaultTimeline( */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, - count: Long): RealmResults { + count: Long, + strict: Boolean): RealmResults { val offsetQuery = liveEvents.where() if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) + if (strict) { + offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } } else { - offsetQuery - .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) - .greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) + if (strict) { + offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } else { + offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) + } } return offsetQuery .limit(count) @@ -589,8 +596,8 @@ internal class DefaultTimeline( } private fun clearAllValues() { - prevDisplayIndex = DISPLAY_INDEX_UNKNOWN - nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + prevDisplayIndex = null + nextDisplayIndex = null builtEvents.clear() builtEventsIdMap.clear() backwardsPaginationState.set(PaginationState()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt index 51fdbfe227..e723a908cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Handler.kt @@ -20,10 +20,10 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper -fun createBackgroundHandler(name: String): Handler = Handler( +internal fun createBackgroundHandler(name: String): Handler = Handler( HandlerThread(name).apply { start() }.looper ) -fun createUIHandler(): Handler = Handler( +internal fun createUIHandler(): Handler = Handler( Looper.getMainLooper() ) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt new file mode 100644 index 0000000000..8c8bd1266f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -0,0 +1,44 @@ +/* + + * 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.riotx.core.utils + +import android.os.Handler + +internal class Debouncer(private val handler: Handler) { + + private val runnables = HashMap() + + fun debounce(identifier: String, millis: Long, r: Runnable): Boolean { + if (runnables.containsKey(identifier)) { + // debounce + val old = runnables[identifier] + handler.removeCallbacks(old) + } + insertRunnable(identifier, r, millis) + return true + } + + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { + val chained = Runnable { + handler.post(r) + runnables.remove(identifier) + } + runnables[identifier] = chained + handler.postDelayed(chained, millis) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt b/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt new file mode 100644 index 0000000000..51316d7e2f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/Handler.kt @@ -0,0 +1,31 @@ +/* + + * 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.riotx.core.utils + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +internal fun createBackgroundHandler(name: String): Handler = Handler( + HandlerThread(name).apply { start() }.looper +) + +internal fun createUIHandler(): Handler = Handler( + Looper.getMainLooper() +) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt index fbb1cfcc4e..87fd32a784 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -19,22 +19,17 @@ package im.vector.riotx.features.home.createdirect import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.user.model.User -import im.vector.matrix.android.internal.util.createUIHandler import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.riotx.R import im.vector.riotx.core.epoxy.EmptyItem_ -import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.noResultItem -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fd83a6f69e..61338e7858 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -202,6 +202,7 @@ class RoomDetailFragment : } } + private val roomDetailArgs: RoomDetailArgs by args() private val glideRequests by lazy { GlideApp.with(this) @@ -221,11 +222,13 @@ class RoomDetailFragment : @Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory @Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter - private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback - private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences + private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback + private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback + private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener + override fun getLayoutResId() = R.layout.fragment_room_detail @@ -374,17 +377,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -413,9 +416,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -430,7 +433,10 @@ class RoomDetailFragment : epoxyVisibilityTracker.attach(recyclerView) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() - scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) + endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + } + scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = null @@ -441,35 +447,32 @@ class RoomDetailFragment : it.dispatchTo(scrollOnHighlightedEventCallback) } - recyclerView.addOnScrollListener( - EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) - }) + recyclerView.addOnScrollListener(endlessScrollListener) recyclerView.setController(timelineEventController) timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index cf483090f1..c272e611a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber import java.util.concurrent.atomic.AtomicReference class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, @@ -28,17 +29,16 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa override fun onChanged(position: Int, count: Int, tag: Any?) { val eventId = scheduledEventId.get() ?: return - val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) - if (positionToScroll != null) { val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { + Timber.v("Scroll to $positionToScroll") // Note: Offset will be from the bottom, since the layoutManager is reversed - layoutManager.scrollToPosition(position) + layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index 8d3a96d8df..f4cfe9eb5a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -18,11 +18,15 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.riotx.core.platform.DefaultListUpdateCallback +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber -class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback { +class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, + private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { override fun onInserted(position: Int, count: Int) { - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { + Timber.v("On inserted $count count at position: $position") + if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { layoutManager.scrollToPosition(0) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 9b9172a6f9..147666345e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -35,12 +35,7 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* -import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem -import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem -import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime @@ -91,8 +86,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } + private var showingForwardLoader = false private val modelCache = arrayListOf() - private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -163,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -182,17 +177,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { - val loaderAdded = LoadingItem_() - .id("forward_loading_item") + val timestamp = System.currentTimeMillis() + showingForwardLoader = LoadingItem_() + .id("forward_loading_item_$timestamp") .addWhen(Timeline.Direction.FORWARDS) val timelineModels = getModels() add(timelineModels) // Avoid displaying two loaders if there is no elements between them - if (!loaderAdded || timelineModels.isNotEmpty()) { + if (!showingForwardLoader || timelineModels.isNotEmpty()) { LoadingItem_() - .id("backward_loading_item") + .id("backward_loading_item_$timestamp") .addWhen(Timeline.Direction.BACKWARDS) } } @@ -224,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -255,7 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){ + val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) @@ -284,6 +280,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { // Search in the cache var realPosition = 0 + if (showingForwardLoader) { + realPosition++ + } for (i in 0 until modelCache.size) { val itemCache = modelCache[i] if (itemCache?.eventId == eventId) { @@ -319,6 +318,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return modelCache.getOrNull(position - offsetValue)?.eventId } + fun isLoadingForward() = showingForwardLoader + private data class CacheItemData( val localId: Long, val eventId: String?,