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 d95408bfb1..52ef816e99 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 @@ -198,9 +198,9 @@ internal class DefaultTimeline( .also { it.addChangeListener(relationsListener) } if (settings.buildReadReceipts) { - hiddenReadReceipts.start(realm, filteredEvents, this) + hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } - hiddenReadMarker.start(realm, filteredEvents, this) + hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this) isReady.set(true) } } @@ -490,9 +490,9 @@ internal class DefaultTimeline( 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 @@ -563,7 +563,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) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index eebb98ca19..03d79c2e00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -46,7 +46,8 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, private var previousDisplayedEventId: String? = null private var hiddenReadMarker: RealmResults? = null - private lateinit var liveEvents: RealmResults + private lateinit var filteredEvents: RealmResults + private lateinit var nonFilteredEvents: RealmResults private lateinit var delegate: Delegate private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet -> @@ -62,12 +63,13 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, } val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@OrderedRealmCollectionChangeListener + ?: return@OrderedRealmCollectionChangeListener + val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId).findFirst() != null val displayIndex = hiddenEvent.root?.displayIndex - if (displayIndex != null) { + if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() + val firstDisplayedEvent = filteredEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() @@ -86,8 +88,12 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String, /** * Start the realm query subscription. Has to be called on an HandlerThread */ - fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { - this.liveEvents = liveEvents + fun start(realm: Realm, + filteredEvents: RealmResults, + nonFilteredEvents: RealmResults, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index f932e6f3c0..0c538f794e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -49,7 +49,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private val correctedReadReceiptsByEvent = HashMap>() private lateinit var hiddenReadReceipts: RealmResults - private lateinit var liveEvents: RealmResults + private lateinit var nonFilteredEvents: RealmResults + private lateinit var filteredEvents: RealmResults private lateinit var delegate: Delegate private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -60,7 +61,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { val eventId = correctedReadReceiptsEventByIndex.get(it, "") - val timelineEvent = liveEvents.where() + val timelineEvent = filteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() @@ -70,12 +71,14 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu } correctedReadReceiptsEventByIndex.clear() correctedReadReceiptsByEvent.clear() - hiddenReadReceipts.forEachIndexed { index, summary -> - val timelineEvent = summary?.timelineEvent?.firstOrNull() - val displayIndex = timelineEvent?.root?.displayIndex - if (displayIndex != null) { + for (index in 0 until hiddenReadReceipts.size) { + val summary = hiddenReadReceipts[index] ?: continue + val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue + val isLoaded = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null + val displayIndex = timelineEvent.root?.displayIndex + if (isLoaded && displayIndex != null) { // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() + val firstDisplayedEvent = filteredEvents.where() .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) .findFirst() @@ -106,8 +109,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu /** * Start the realm query subscription. Has to be called on an HandlerThread */ - fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { - this.liveEvents = liveEvents + fun start(realm: Realm, + filteredEvents: RealmResults, + nonFilteredEvents: RealmResults, + delegate: Delegate) { + this.filteredEvents = filteredEvents + this.nonFilteredEvents = nonFilteredEvents this.delegate = delegate // We are looking for read receipts set on hidden events. // We only accept those with a timelineEvent (so coming from pagination/sync). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index fdbaa2ab1b..c052cf7146 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -40,14 +40,18 @@ internal class RoomFullyReadHandler @Inject constructor() { readMarkerId = content.eventId } // Remove the old markers if any - val oldReadMarkerEvents = TimelineEventEntity.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH).isNotNull(TimelineEventEntityFields.READ_MARKER.`$`).findAll() + val oldReadMarkerEvents = TimelineEventEntity + .where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH) + .isNotNull(TimelineEventEntityFields.READ_MARKER.`$`) + .findAll() + oldReadMarkerEvents.forEach { it.readMarker = null } val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { this.eventId = content.eventId } // Attach to timelineEvent if known - val timelineEventEntity = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findFirst() - timelineEventEntity?.readMarker = readMarkerEntity + val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll() + timelineEventEntities.forEach { it.readMarker = readMarkerEntity } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt index 7364c254a8..e23a9084a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -80,7 +80,9 @@ class ReadMarkerHelper @Inject constructor() { val newJumpToReadMarkerVisible = if (readMarkerId == null) { false } else { - val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) + val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId) + ?: readMarkerId + val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId) if (positionOfReadMarker == null) { nonNullState.timeline?.isLive == true && lastVisibleItem > 0 } else { 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 f490bf66ab..e391c0d13f 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 @@ -250,6 +250,7 @@ class RoomDetailFragment : if (scrollPosition == null) { scrollOnHighlightedEventCallback.scheduleScrollTo(it) } else { + recyclerView.stopScroll() layoutManager.scrollToPosition(scrollPosition) } } @@ -445,7 +446,7 @@ class RoomDetailFragment : layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) - scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) + scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(recyclerView, layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = null recyclerView.setHasFixedSize(true) @@ -958,7 +959,7 @@ class RoomDetailFragment : val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItem = timelineEventController.adapter.getModelAtPosition(firstVisibleItemPosition) val nextReadMarkerId = when (firstVisibleItem) { - is BaseEventItem -> firstVisibleItem.getEventId() + is BaseEventItem -> firstVisibleItem.getEventIds().firstOrNull() else -> null } if (nextReadMarkerId != null) { 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 08add3f0c7..52ebba817a 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 @@ -17,9 +17,11 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -29,7 +31,8 @@ import java.util.concurrent.atomic.AtomicReference /** * This handles scrolling to an event which wasn't yet loaded when scheduled. */ -class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, +class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, + private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val scheduledEventId = AtomicReference() @@ -56,6 +59,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { Timber.v("Scroll to $positionToScroll") + recyclerView.stopScroll() layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) 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 3dda4b333c..519d1f71c7 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 @@ -50,7 +50,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val mergedHeaderItemFactory: MergedHeaderItemFactory, @TimelineEventControllerHandler private val backgroundHandler: Handler -) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { +) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { fun onLoadMore(direction: Timeline.Direction) @@ -91,6 +91,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private var showingForwardLoader = false + // Map eventId to adapter position + private val adapterPositionMapping = HashMap() private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false @@ -98,6 +100,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec var callback: Callback? = null + private val listUpdateCallback = object : ListUpdateCallback { override fun onChanged(position: Int, count: Int, payload: Any?) { @@ -141,9 +144,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } init { + addInterceptor(this) requestModelBuild() } + // Update position when we are building new items + override fun intercept(models: MutableList>) { + adapterPositionMapping.clear() + models.forEachIndexed { index, epoxyModel -> + if (epoxyModel is BaseEventItem) { + epoxyModel.getEventIds().forEach { + adapterPositionMapping[it] = index + } + } + } + } + fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { if (timeline != viewState.timeline) { timeline = viewState.timeline @@ -161,7 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -186,6 +202,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineMediaSizeProvider.recyclerView = recyclerView } + override fun buildModels() { val timestamp = System.currentTimeMillis() showingForwardLoader = LoadingItem_() @@ -232,8 +249,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) } } @@ -269,13 +286,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + readMarkerVisible = readMarkerVisible, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } @@ -314,30 +331,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) { - // Search in the cache - if (eventId == null) { - return null - } - var realPosition = 0 - if (showingForwardLoader) { - realPosition++ - } - for (i in 0 until modelCache.size) { - val itemCache = modelCache[i] ?: continue - if (itemCache.eventId == eventId) { - return realPosition - } - if (itemCache.eventModel != null && !mergedHeaderItemFactory.isCollapsed(itemCache.localId)) { - realPosition++ - } - if (itemCache.mergedHeaderModel != null) { - realPosition++ - } - if (itemCache.formattedDayModel != null) { - realPosition++ - } - } - return null + return adapterPositionMapping[eventId] } fun isLoadingForward() = showingForwardLoader diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index c7d75998a2..64547bbcf7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,7 +29,6 @@ import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute -import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider @@ -158,8 +157,8 @@ abstract class AbsMessageItem : BaseEventItem() { return true } - override fun getEventId(): String? { - return attributes.informationData.eventId + override fun getEventIds(): List { + return listOf(attributes.informationData.eventId) } protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 7727b07cd8..c6e813e878 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -44,12 +44,15 @@ abstract class BaseEventItem : VectorEpoxyModel override fun bind(holder: H) { super.bind(holder) - holder holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.checkableBackground.isChecked = highlighted } - abstract fun getEventId(): String? + /** + * Returns the eventIds associated with the EventItem. + * Will generally get only one, but it handles the merging items. + */ + abstract fun getEventIds(): List abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt index 0e2d87512b..cd1e39d37f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/DefaultItem.kt @@ -55,7 +55,7 @@ abstract class DefaultItem : BaseEventItem() { holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } - override fun getEventId(): String? { + override fun getEventIds(): List { return informationData.eventId } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 727a585d71..a15d9fa333 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -90,8 +90,8 @@ abstract class MergedHeaderItem : BaseEventItem() { } - override fun getEventId(): String? { - return attributes.mergeData.firstOrNull()?.eventId + override fun getEventIds(): List { + return attributes.mergeData.map { it.eventId } } data class Data( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index c398970e8e..2906eb58ba 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -70,8 +70,8 @@ abstract class NoticeItem : BaseEventItem() { } - override fun getEventId(): String? { - return attributes.informationData.eventId + override fun getEventIds(): List { + return listOf(attributes.informationData.eventId) } override fun getViewType() = STUB_ID