Timeline: try to fix some issues with permalink [WIP]
This commit is contained in:
parent
f4ab770be9
commit
5d6d0202a9
|
@ -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<TimelineEvent>(ArrayList())
|
||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
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<TimelineEventEntity> {
|
||||
count: Long,
|
||||
strict: Boolean): RealmResults<TimelineEventEntity> {
|
||||
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())
|
||||
|
|
|
@ -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()
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CacheItemData?>()
|
||||
|
||||
private var currentSnapshot: List<TimelineEvent> = 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?,
|
||||
|
|
Loading…
Reference in New Issue