Timeline: try to fix some issues with permalink [WIP]

This commit is contained in:
ganfra 2019-09-14 14:11:41 +02:00
parent f4ab770be9
commit 5d6d0202a9
9 changed files with 178 additions and 93 deletions

View File

@ -42,10 +42,11 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.max
import kotlin.math.min
private const val MIN_FETCHING_COUNT = 30 private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
internal class DefaultTimeline( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
@ -85,8 +86,8 @@ internal class DefaultTimeline(
private var roomEntity: RoomEntity? = null private var roomEntity: RoomEntity? = null
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int? = null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsPaginationState = AtomicReference(PaginationState()) private val backwardsPaginationState = AtomicReference(PaginationState())
@ -222,6 +223,7 @@ internal class DefaultTimeline(
if (isStarted.compareAndSet(true, false)) { if (isStarted.compareAndSet(true, false)) {
eventDecryptor.destroy() eventDecryptor.destroy()
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
cancelableBag.cancel() cancelableBag.cancel()
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
@ -303,11 +305,8 @@ internal class DefaultTimeline(
private fun hasMoreInCache(direction: Timeline.Direction): Boolean { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm -> return Realm.getInstance(realmConfiguration).use { localRealm ->
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
?: return false ?: return false
if (direction == Timeline.Direction.FORWARDS) { if (direction == Timeline.Direction.FORWARDS) {
if (findCurrentChunk(localRealm)?.isLastForward == true) {
return false
}
val firstEvent = builtEvents.firstOrNull() ?: return true val firstEvent = builtEvents.firstOrNull() ?: return true
firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex
} else { } else {
@ -334,16 +333,17 @@ internal class DefaultTimeline(
* This has to be called on TimelineThread as it access realm live results * This has to be called on TimelineThread as it access realm live results
* @return true if createSnapshot should be posted * @return true if createSnapshot should be posted
*/ */
private fun paginateInternal(startDisplayIndex: Int, private fun paginateInternal(startDisplayIndex: Int?,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Int): Boolean { count: Int,
strict: Boolean = false): Boolean {
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } 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) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
if (shouldFetchMore) { if (shouldFetchMore) {
val newRequestedCount = count - builtCount val newRequestedCount = count - builtCount
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount)
executePaginationTask(direction, fetchingCount) executePaginationTask(direction, fetchingCount)
} else { } else {
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
@ -404,20 +404,19 @@ internal class DefaultTimeline(
.findFirst() .findFirst()
shouldFetchInitialEvent = initialEvent == null shouldFetchInitialEvent = initialEvent == null
initialEvent?.root?.displayIndex initialEvent?.root?.displayIndex
} ?: DISPLAY_INDEX_UNKNOWN }
prevDisplayIndex = initialDisplayIndex prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex
val currentInitialEventId = initialEventId val currentInitialEventId = initialEventId
if (currentInitialEventId != null && shouldFetchInitialEvent) { if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId) fetchEvent(currentInitialEventId)
} else { } else {
val count = Math.min(settings.initialSize, liveEvents.size) val count = min(settings.initialSize, liveEvents.size)
if (isLive) { if (isLive) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
} else { } else {
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2) paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true)
} }
} }
postSnapshot() postSnapshot()
@ -429,9 +428,9 @@ internal class DefaultTimeline(
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction) ?: return val token = getTokenLive(direction) ?: return
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = token,
direction = direction.toPaginationDirection(), direction = direction.toPaginationDirection(),
limit = limit) limit = limit)
Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask cancelableBag += paginationTask
@ -479,14 +478,15 @@ internal class DefaultTimeline(
* This has to be called on TimelineThread as it access realm live results * This has to be called on TimelineThread as it access realm live results
* @return number of items who have been added * @return number of items who have been added
*/ */
private fun buildTimelineEvents(startDisplayIndex: Int, private fun buildTimelineEvents(startDisplayIndex: Int?,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Long): Int { count: Long,
if (count < 1) { strict: Boolean = false): Int {
if (count < 1 || startDisplayIndex == null) {
return 0 return 0
} }
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val offsetResults = getOffsetResults(startDisplayIndex, direction, count) val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict)
if (offsetResults.isEmpty()) { if (offsetResults.isEmpty()) {
return 0 return 0
} }
@ -501,7 +501,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
} }
@ -527,16 +527,23 @@ internal class DefaultTimeline(
*/ */
private fun getOffsetResults(startDisplayIndex: Int, private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Long): RealmResults<TimelineEventEntity> { count: Long,
strict: Boolean): RealmResults<TimelineEventEntity> {
val offsetQuery = liveEvents.where() val offsetQuery = liveEvents.where()
if (direction == Timeline.Direction.BACKWARDS) { if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) if (strict) {
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
}
} else { } else {
offsetQuery offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING) if (strict) {
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex) offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
}
} }
return offsetQuery return offsetQuery
.limit(count) .limit(count)
@ -589,8 +596,8 @@ internal class DefaultTimeline(
} }
private fun clearAllValues() { private fun clearAllValues() {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN prevDisplayIndex = null
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN nextDisplayIndex = null
builtEvents.clear() builtEvents.clear()
builtEventsIdMap.clear() builtEventsIdMap.clear()
backwardsPaginationState.set(PaginationState()) backwardsPaginationState.set(PaginationState())

View File

@ -20,10 +20,10 @@ import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.Looper import android.os.Looper
fun createBackgroundHandler(name: String): Handler = Handler( internal fun createBackgroundHandler(name: String): Handler = Handler(
HandlerThread(name).apply { start() }.looper HandlerThread(name).apply { start() }.looper
) )
fun createUIHandler(): Handler = Handler( internal fun createUIHandler(): Handler = Handler(
Looper.getMainLooper() Looper.getMainLooper()
) )

View File

@ -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<String, Runnable>()
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)
}
}

View File

@ -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()
)

View File

@ -19,22 +19,17 @@ package im.vector.riotx.features.home.createdirect
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User 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.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.EmptyItem_ 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.loadingItem
import im.vector.riotx.core.epoxy.noResultItem 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.resources.StringProvider
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject

View File

@ -202,6 +202,7 @@ class RoomDetailFragment :
} }
} }
private val roomDetailArgs: RoomDetailArgs by args() private val roomDetailArgs: RoomDetailArgs by args()
private val glideRequests by lazy { private val glideRequests by lazy {
GlideApp.with(this) GlideApp.with(this)
@ -221,11 +222,13 @@ class RoomDetailFragment :
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory @Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory @Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@Inject lateinit var vectorPreferences: VectorPreferences @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 override fun getLayoutResId() = R.layout.fragment_room_detail
@ -374,17 +377,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody val document = parser.parse(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document) formattedBody = eventHtmlRenderer.render(document)
} }
composerLayout.composerRelatedMessageContent.text = formattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody ?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand { composerLayout.expand {
@ -413,9 +416,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return ?: return
//TODO check if already reacted with that? //TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
} }
@ -430,7 +433,10 @@ class RoomDetailFragment :
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() 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) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
@ -441,35 +447,32 @@ class RoomDetailFragment :
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
} }
recyclerView.addOnScrollListener( recyclerView.addOnScrollListener(endlessScrollListener)
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
})
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this timelineEventController.callback = this
if (vectorPreferences.swipeToReplyIsEnabled()) { if (vectorPreferences.swipeToReplyIsEnabled()) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply, R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler { object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let { (model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
} }
override fun canSwipeModel(model: EpoxyModel<*>): Boolean { override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) { return when (model) {
is MessageFileItem, is MessageFileItem,
is MessageImageVideoItem, is MessageImageVideoItem,
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
} }
}) })
val touchHelper = ItemTouchHelper(swipeCallback) val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView) touchHelper.attachToRecyclerView(recyclerView)
} }

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, 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?) { override fun onChanged(position: Int, count: Int, tag: Any?) {
val eventId = scheduledEventId.get() ?: return val eventId = scheduledEventId.get() ?: return
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
if (positionToScroll != null) { if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
// Do not scroll it item is already visible // Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) { if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
Timber.v("Scroll to $positionToScroll")
// Note: Offset will be from the bottom, since the layoutManager is reversed // Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPosition(position) layoutManager.scrollToPosition(positionToScroll)
} }
scheduledEventId.set(null) scheduledEventId.set(null)
} }

View File

@ -18,11 +18,15 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback 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) { 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) layoutManager.scrollToPosition(0)
} }
} }

View File

@ -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.MergedHeaderItemFactory
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory 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.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotx.features.home.room.detail.timeline.item.*
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.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
@ -91,8 +86,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onUrlLongClicked(url: String): Boolean fun onUrlLongClicked(url: String): Boolean
} }
private var showingForwardLoader = false
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList() private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false private var inSubmitList: Boolean = false
private var timeline: Timeline? = null private var timeline: Timeline? = null
@ -163,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
synchronized(modelCache) { synchronized(modelCache) {
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) { || modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null modelCache[i] = null
} }
} }
@ -182,17 +177,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
override fun buildModels() { override fun buildModels() {
val loaderAdded = LoadingItem_() val timestamp = System.currentTimeMillis()
.id("forward_loading_item") showingForwardLoader = LoadingItem_()
.id("forward_loading_item_$timestamp")
.addWhen(Timeline.Direction.FORWARDS) .addWhen(Timeline.Direction.FORWARDS)
val timelineModels = getModels() val timelineModels = getModels()
add(timelineModels) add(timelineModels)
// Avoid displaying two loaders if there is no elements between them // Avoid displaying two loaders if there is no elements between them
if (!loaderAdded || timelineModels.isNotEmpty()) { if (!showingForwardLoader || timelineModels.isNotEmpty()) {
LoadingItem_() LoadingItem_()
.id("backward_loading_item") .id("backward_loading_item_$timestamp")
.addWhen(Timeline.Direction.BACKWARDS) .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 // 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. // We then are sure we always have items up to date.
if (modelCache[position] == null if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null || modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) { || modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot) modelCache[position] = buildItemModels(position, currentSnapshot)
} }
} }
@ -255,7 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) 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() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
@ -284,6 +280,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
// Search in the cache // Search in the cache
var realPosition = 0 var realPosition = 0
if (showingForwardLoader) {
realPosition++
}
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
val itemCache = modelCache[i] val itemCache = modelCache[i]
if (itemCache?.eventId == eventId) { if (itemCache?.eventId == eventId) {
@ -319,6 +318,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return modelCache.getOrNull(position - offsetValue)?.eventId return modelCache.getOrNull(position - offsetValue)?.eventId
} }
fun isLoadingForward() = showingForwardLoader
private data class CacheItemData( private data class CacheItemData(
val localId: Long, val localId: Long,
val eventId: String?, val eventId: String?,