From 5e07e96bdb8857de41b2bbfc99cc8dde4b39e2ed Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Nov 2019 19:49:48 +0100 Subject: [PATCH 01/13] Read marker: start reworking how we manage it [WIP] --- .../session/room/timeline/TimelineEvent.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 16 +-- .../database/mapper/TimelineEventMapper.kt | 5 +- .../database/model/ReadMarkerEntity.kt | 5 - .../database/model/TimelineEventEntity.kt | 3 +- .../session/room/timeline/DefaultTimeline.kt | 23 +-- .../room/timeline/DefaultTimelineService.kt | 21 ++- .../room/timeline/TimelineHiddenReadMarker.kt | 133 ------------------ .../session/sync/RoomFullyReadHandler.kt | 18 +-- .../riotx/core/extensions/TimelineEvent.kt | 4 - .../riotx/core/ui/views/ReadMarkerView.kt | 89 ------------ .../home/room/detail/ReadMarkerHelper.kt | 35 ----- .../home/room/detail/RoomDetailAction.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 36 +---- .../home/room/detail/RoomDetailViewModel.kt | 111 +++++++++------ .../home/room/detail/RoomDetailViewState.kt | 3 +- .../timeline/TimelineEventController.kt | 87 ++++++------ .../timeline/factory/DefaultItemFactory.kt | 3 +- .../timeline/factory/EncryptedItemFactory.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 11 -- .../timeline/factory/MessageItemFactory.kt | 5 +- .../timeline/factory/NoticeItemFactory.kt | 3 +- .../timeline/factory/TimelineItemFactory.kt | 13 +- .../helper/MessageInformationDataFactory.kt | 8 +- ...lineEventVisibilityStateChangedListener.kt | 13 ++ .../detail/timeline/item/AbsMessageItem.kt | 15 -- .../detail/timeline/item/BaseEventItem.kt | 2 - .../detail/timeline/item/MergedHeaderItem.kt | 20 --- .../timeline/item/MessageInformationData.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 18 --- .../timeline/item/TimelineReadMarkerItem.kt | 31 ++++ .../res/layout/item_timeline_event_base.xml | 13 +- .../item_timeline_event_base_noinfo.xml | 30 +--- .../res/layout/item_timeline_read_marker.xml | 18 +++ 34 files changed, 236 insertions(+), 570 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt delete mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_read_marker.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ad747efee9..ed7f49aa46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -41,8 +41,7 @@ data class TimelineEvent( val isUniqueDisplayName: Boolean, val senderAvatar: String?, val annotations: EventAnnotationsSummary? = null, - val readReceipts: List = emptyList(), - val hasReadMarker: Boolean = false + val readReceipts: List = emptyList() ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index e9ffa140c9..826b35254e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String, val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId, roomId) + ?: ReadReceiptsSummaryEntity(eventId, roomId) // Update RR for the sender of a new message with a dummy one @@ -168,7 +167,6 @@ internal fun ChunkEntity.add(roomId: String, it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.readReceipts = readReceiptsSummaryEntity - it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst() } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) @@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 8046ecbff0..9959f940b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS } return TimelineEvent( root = timelineEventEntity.root?.asDomain() - ?: Event("", timelineEventEntity.eventId), + ?: Event("", timelineEventEntity.eventId), annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.root?.displayIndex ?: 0, @@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderAvatar = timelineEventEntity.senderAvatar, readReceipts = readReceipts?.sortedByDescending { it.originServerTs - } ?: emptyList(), - hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true + } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt index 9e78c94f88..4d16d120d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReadMarkerEntity.kt @@ -17,8 +17,6 @@ package im.vector.matrix.android.internal.database.model import io.realm.RealmObject -import io.realm.RealmResults -import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey internal open class ReadMarkerEntity( @@ -27,8 +25,5 @@ internal open class ReadMarkerEntity( var eventId: String = "" ) : RealmObject() { - @LinkingObjects("readMarker") - val timelineEvent: RealmResults? = null - companion object } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index fd3a427781..235910b1ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, var senderMembershipEvent: EventEntity? = null, - var readReceipts: ReadReceiptsSummaryEntity? = null, - var readMarker: ReadMarkerEntity? = null + var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { @LinkingObjects("timelineEvents") 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 4127e43540..aa4bd42bf7 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 @@ -74,9 +74,8 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts, - private val hiddenReadMarker: TimelineHiddenReadMarker -) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate { + private val hiddenReadReceipts: TimelineHiddenReadReceipts +) : Timeline, TimelineHiddenReadReceipts.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -197,7 +196,6 @@ internal class DefaultTimeline( if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } - hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this) isReady.set(true) } } @@ -217,7 +215,6 @@ internal class DefaultTimeline( if (this::filteredEvents.isInitialized) { filteredEvents.removeAllChangeListeners() } - hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } @@ -298,7 +295,7 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } -// TimelineHiddenReadReceipts.Delegate + // TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { return rebuildEvent(eventId) { te -> @@ -310,19 +307,7 @@ internal class DefaultTimeline( postSnapshot() } -// TimelineHiddenReadMarker.Delegate - - override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean { - return rebuildEvent(eventId) { te -> - te.copy(hasReadMarker = hasReadMarker) - } - } - - override fun onReadMarkerUpdated() { - postSnapshot() - } - -// Private methods ***************************************************************************** + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { return builtEventsIdMap[eventId]?.let { builtIndex -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 3bd67d38c3..d92dbd66be 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { return DefaultTimeline(roomId, - eventId, - monarchy.realmConfiguration, - taskExecutor, - contextOfEventTask, - clearUnlinkedEventsTask, - paginationTask, - cryptoService, - timelineEventMapper, - settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), - TimelineHiddenReadMarker(roomId, settings) + eventId, + monarchy.realmConfiguration, + taskExecutor, + contextOfEventTask, + clearUnlinkedEventsTask, + paginationTask, + cryptoService, + timelineEventMapper, + settings, + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) ) } 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 deleted file mode 100644 index 4f80883bf9..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - - * 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.matrix.android.internal.session.room.timeline - -import im.vector.matrix.android.api.session.room.timeline.TimelineSettings -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity -import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent -import im.vector.matrix.android.internal.database.query.where -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.RealmResults - -/** - * This class is responsible for handling the read marker for hidden events. - * When an hidden event has read marker, we want to transfer it on the first older displayed event. - * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. - */ -internal class TimelineHiddenReadMarker constructor(private val roomId: String, - private val settings: TimelineSettings) { - - interface Delegate { - fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean - fun onReadMarkerUpdated() - } - - private var previousDisplayedEventId: String? = null - private var hiddenReadMarker: RealmResults? = null - - private lateinit var filteredEvents: RealmResults - private lateinit var nonFilteredEvents: RealmResults - private lateinit var delegate: Delegate - - private val readMarkerListener = OrderedRealmCollectionChangeListener> { readMarkers, changeSet -> - if (!readMarkers.isLoaded || !readMarkers.isValid) { - return@OrderedRealmCollectionChangeListener - } - var hasChange = false - if (changeSet.deletions.isNotEmpty()) { - previousDisplayedEventId?.also { - hasChange = delegate.rebuildEvent(it, false) - previousDisplayedEventId = null - } - } - val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener - val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@OrderedRealmCollectionChangeListener - - val isLoaded = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId) - .findFirst() != null - - val displayIndex = hiddenEvent.root?.displayIndex - if (isLoaded && displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should rebuild this one with marker - if (firstDisplayedEvent != null) { - previousDisplayedEventId = firstDisplayedEvent.eventId - hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) - } - } - if (hasChange) { - delegate.onReadMarkerUpdated() - } - } - - /** - * Start the realm query subscription. Has to be called on an HandlerThread - */ - 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). - hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId) - .isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(readMarkerListener) } - } - - /** - * Dispose the realm query subscription. Has to be called on an HandlerThread - */ - fun dispose() { - this.hiddenReadMarker?.removeAllChangeListeners() - } - - /** - * We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - if (settings.filterTypes) { - not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() - } - if (settings.filterEdits) { - like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) - } - endGroup() - return this - } -} 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 853774460f..61ae8b9925 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 @@ -16,14 +16,10 @@ package im.vector.matrix.android.internal.session.sync -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.getOrCreate -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.read.FullyReadContent import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() { RoomSummaryEntity.getOrCreate(realm, roomId).apply { 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() - - oldReadMarkerEvents.forEach { it.readMarker = null } - val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { + ReadMarkerEntity.getOrCreate(realm, roomId).apply { this.eventId = content.eventId } - // Attach to timelineEvent if known - val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll() - timelineEventEntities.forEach { it.readMarker = readMarkerEntity } } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt index 387105c480..388ec9bebe 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt @@ -24,7 +24,3 @@ fun TimelineEvent.canReact(): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted() } - -fun TimelineEvent.displayReadMarker(myUserId: String): Boolean { - return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null -} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt deleted file mode 100644 index 0fb8b55250..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - - * 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.ui.views - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import im.vector.riotx.R -import kotlinx.coroutines.* - -private const val DELAY_IN_MS = 1_000L - -class ReadMarkerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - interface Callback { - fun onReadMarkerLongBound(isDisplayed: Boolean) - } - - private var eventId: String? = null - private var callback: Callback? = null - private var callbackDispatcherJob: Job? = null - - fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { - this.eventId = eventId - this.callback = readMarkerCallback - if (displayReadMarker) { - startAnimation() - } else { - this.animation?.cancel() - this.visibility = INVISIBLE - } - if (hasReadMarker) { - callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { - delay(DELAY_IN_MS) - callback?.onReadMarkerLongBound(displayReadMarker) - } - } - } - - fun unbind() { - this.callbackDispatcherJob?.cancel() - this.callback = null - this.eventId = null - this.animation?.cancel() - this.visibility = INVISIBLE - } - - private fun startAnimation() { - if (animation == null) { - animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim) - animation.startOffset = DELAY_IN_MS / 2 - animation.duration = DELAY_IN_MS / 2 - animation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) { - } - - override fun onAnimationEnd(animation: Animation) { - visibility = INVISIBLE - } - - override fun onAnimationRepeat(animation: Animation) {} - }) - } - visibility = VISIBLE - animation.start() - } -} 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 7b3ebeb71c..98556cc7fa 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 @@ -28,49 +28,14 @@ class ReadMarkerHelper @Inject constructor() { lateinit var timelineEventController: TimelineEventController lateinit var layoutManager: LinearLayoutManager var callback: Callback? = null - - private var onReadMarkerLongDisplayed = false private var jumpToReadMarkerVisible = false - private var readMarkerVisible: Boolean = true private var state: RoomDetailViewState? = null - fun readMarkerVisible(): Boolean { - return readMarkerVisible - } - - fun onResume() { - onReadMarkerLongDisplayed = false - } - - fun onReadMarkerLongDisplayed() { - onReadMarkerLongDisplayed = true - } - fun updateWith(newState: RoomDetailViewState) { state = newState - checkReadMarkerVisibility() checkJumpToReadMarkerVisibility() } - fun onTimelineScrolled() { - checkJumpToReadMarkerVisibility() - } - - private fun checkReadMarkerVisibility() { - val nonNullState = this.state ?: return - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - readMarkerVisible = if (!onReadMarkerLongDisplayed) { - true - } else { - if (nonNullState.timeline?.isLive == false) { - true - } else { - !(firstVisibleItem == 0 && lastVisibleItem > 0) - } - } - } - private fun checkJumpToReadMarkerVisibility() { val nonNullState = this.state ?: return val lastVisibleItem = layoutManager.findLastVisibleItemPosition() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 0a6321dd57..c1743ae3fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -35,13 +35,15 @@ sealed class RoomDetailAction : VectorViewModelAction { data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction() - data class SetReadMarkerAction(val eventId: String) : RoomDetailAction() object MarkAllAsRead : RoomDetailAction() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction() data class HandleTombstoneEvent(val event: Event) : RoomDetailAction() object AcceptInvite : RoomDetailAction() object RejectInvite : RoomDetailAction() + object EnterTrackingUnreadMessagesState : RoomDetailAction() + object ExitTrackingUnreadMessagesState : RoomDetailAction() + data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() 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 31278a1fff..e0c43b9e74 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 @@ -292,6 +292,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { + roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) debouncer.cancelAll() super.onDestroy() } @@ -299,6 +300,7 @@ class RoomDetailFragment @Inject constructor( private fun setupJumpToBottomView() { jumpToBottomView.visibility = View.INVISIBLE jumpToBottomView.setOnClickListener { + roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE withState(roomDetailViewModel) { state -> if (state.timeline?.isLive == false) { @@ -428,7 +430,6 @@ class RoomDetailFragment @Inject constructor( } override fun onResume() { - readMarkerHelper.onResume() super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) } @@ -484,13 +485,6 @@ class RoomDetailFragment @Inject constructor( recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { - updateJumpToBottomViewVisibility() - } - readMarkerHelper.onTimelineScrolled() - } - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { when (newState) { RecyclerView.SCROLL_STATE_IDLE -> { @@ -668,7 +662,7 @@ class RoomDetailFragment @Inject constructor( val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { scrollOnHighlightedEventCallback.timeline = state.timeline - timelineEventController.update(state, readMarkerHelper.readMarkerVisible()) + timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -1024,28 +1018,8 @@ class RoomDetailFragment @Inject constructor( .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) { - readMarkerHelper.onReadMarkerLongDisplayed() - val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - if (readMarkerIndex > lastVisibleItemPosition) { - return - } - val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() - var nextReadMarkerId: String? = null - for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) { - val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition) - if (timelineItem is BaseEventItem) { - val eventId = timelineItem.getEventIds().firstOrNull() ?: continue - if (!LocalEcho.isLocalEchoId(eventId)) { - nextReadMarkerId = eventId - break - } - } - } - if (nextReadMarkerId != null) { - roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId)) - } + override fun onReadMarkerDisplayed() { + roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } // AutocompleteUserPresenter.Callback diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a264e0d06c..e1ff991797 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -64,6 +65,7 @@ import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber import java.io.File import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, @@ -102,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Slot to keep a pending uri during permission request var pendingUri: Uri? = null + private var trackUnreadMessages = AtomicBoolean(false) + private var mostRecentDisplayedEvent: TimelineEvent? = null + @AssistedInject.Factory interface Factory { fun create(initialState: RoomDetailViewState): RoomDetailViewModel @@ -120,6 +125,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { + getSnapshotOfReadMarkerId() observeSyncState() observeRoomSummary() observeEventDisplayedActions() @@ -132,33 +138,47 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadFile -> handleDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() - is RoomDetailAction.ResendAll -> handleResendAll() - is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadFile -> handleDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() + is RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> handleExitTrackingUnreadMessages() + } + } + + private fun handleEnterTrackingUnreadMessages() { + trackUnreadMessages.set(true) + } + + private fun handleExitTrackingUnreadMessages() { + if (trackUnreadMessages.getAndSet(false)) { + mostRecentDisplayedEvent?.root?.eventId?.also { + room.setReadMarker(it, callback = object : MatrixCallback {}) + } + mostRecentDisplayedEvent = null } } @@ -685,26 +705,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.maxBy { it.event.displayIndex } - mostRecentEvent?.event?.root?.eventId?.let { eventId -> + val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy + val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent + if (trackUnreadMessages.get()) { + if (globalMostRecentDisplayedEvent == null) { + mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent + } else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) { + mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent + } + } + bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) .disposeOnClear() } - private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState { - var readMarkerId = action.eventId - val indexOfEvent = timeline.getIndexOfEvent(readMarkerId) - // force to set the read marker on the next event - if (indexOfEvent != null) { - timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> - readMarkerId = eventIdOfNext - } - } - room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) - } - private fun handleMarkAllAsRead() { room.markAllAsRead(object : MatrixCallback {}) } @@ -759,6 +775,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun getSnapshotOfReadMarkerId() { + room.rx().liveRoomSummary() + .unwrap() + .filter { it.readMarkerId != null } + .take(1) + .subscribe { roomSummary -> + setState { + copy(readMarkerIdSnapshot = roomSummary.readMarkerId) + } + } + .disposeOnClear() + } + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 03110858a1..e476545aa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,7 +52,8 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val highlightedEventId: String? = null + val highlightedEventId: String? = null, + val readMarkerIdSnapshot: String? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) 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 be2f1dd7e4..9614d2aba7 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 @@ -31,15 +31,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime -import im.vector.riotx.core.utils.DimensionConverter -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.RoomDetailViewState 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.TimelineEventDiffUtilCallback -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull +import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -50,8 +45,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, - private val avatarRenderer: AvatarRenderer, - private val dimensionConverter: DimensionConverter, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -86,7 +79,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) + fun onReadMarkerDisplayed() } interface UrlClickCallback { @@ -101,6 +94,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null + private var readMarkerIdSnapshot: String? = null var callback: Callback? = null @@ -163,7 +157,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { + fun update(viewState: RoomDetailViewState) { if (timeline != viewState.timeline) { timeline = viewState.timeline timeline?.listener = this @@ -188,8 +182,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.readMarkerVisible != readMarkerVisible) { - this.readMarkerVisible = readMarkerVisible + if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) { + this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot requestModelBuild = true } if (requestModelBuild) { @@ -197,7 +191,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var readMarkerVisible: Boolean = false private var eventIdToHighlight: String? = null override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { @@ -247,42 +240,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun getModels(): List> { synchronized(modelCache) { + val readMarkerIdSnapshot = this.readMarkerIdSnapshot + val displayableReadMarkerId = if (readMarkerIdSnapshot != null) { + timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) + } else { + null + } (0 until modelCache.size).forEach { position -> - // 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 additional models // 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] = buildItemModels(position, currentSnapshot) + if (modelCache[position] == null || modelCache[position]?.hasAdditionalModel() == true) { + modelCache[position] = buildItemModels(position, currentSnapshot, displayableReadMarkerId) } } - return modelCache - .map { - val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { - null - } else { - it.eventModel - } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) - } - .flatten() - .filterNotNull() } + return modelCache + .map { + val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { + null + } else { + it.eventModel + } + listOf(it?.readMarkerModel, eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } + .flatten() + .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { + private fun buildItemModels(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - // Don't show read marker if it's on first item - val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) { - false - } else { - readMarkerVisible - } - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } @@ -290,7 +281,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec nextEvent = nextEvent, items = items, addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, currentPosition = currentPosition, eventIdToHighlight = eventIdToHighlight, callback = callback @@ -298,8 +288,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) + val readMarkerItem = buildReadMarkerItem(currentPosition, event, displayableReadMarkerId) + return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) + } - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) + private fun buildReadMarkerItem(currentPosition: Int, event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { + return if (currentPosition != 0 && event.root.eventId == displayableReadMarkerId) { + TimelineReadMarkerItem_() + .also { + it.id(event.localId) + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + } else { + null + } } private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { @@ -342,6 +344,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventId: String?, val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null - ) + val formattedDayModel: DaySeparatorItem? = null, + val readMarkerModel: TimelineReadMarkerItem? = null + ) { + fun hasAdditionalModel() = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 1ae47f9c22..94d7812512 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -46,7 +46,6 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava fun create(event: TimelineEvent, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?, exception: Exception? = null): DefaultItem { val text = if (exception == null) { @@ -54,7 +53,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava } else { "an exception occurred when rendering the event ${event.root.eventId}" } - val informationData = informationDataFactory.create(event, null, readMarkerVisible) + val informationData = informationDataFactory.create(event, null) return create(text, informationData, highlight, callback) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index c7aca768dc..512fffa29e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -42,7 +42,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { event.root.eventId ?: return null @@ -66,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it - val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) + val informationData = messageInformationDataFactory.create(event, nextEvent) val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 51364e24c9..a2e979a08d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -36,7 +36,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act nextEvent: TimelineEvent?, items: List, addDaySeparator: Boolean, - readMarkerVisible: Boolean, currentPosition: Int, eventIdToHighlight: String?, callback: TimelineEventController.Callback?, @@ -50,20 +49,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act null } else { var highlighted = false - var readMarkerId: String? = null - var showReadMarker = false val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } - if (readMarkerId == null && mergedEvent.hasReadMarker) { - readMarkerId = mergedEvent.root.eventId - } - if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) { - showReadMarker = true - } val senderAvatar = mergedEvent.senderAvatar val senderName = mergedEvent.getDisambiguatedDisplayName() val data = MergedHeaderItem.Data( @@ -96,8 +87,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act mergeItemCollapseStates[event.localId] = it requestModelBuild() }, - readMarkerId = readMarkerId, - showReadMarker = isCollapsed && showReadMarker, readReceiptsCallback = callback ) MergedHeaderItem_() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index de2686de04..9c96f17022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -69,12 +69,11 @@ class MessageItemFactory @Inject constructor( fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible) + val informationData = messageInformationDataFactory.create(event, nextEvent) if (event.root.isRedacted()) { // message is redacted @@ -91,7 +90,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event - return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) + return noticeItemFactory.create(event, highlight, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 8768da26cf..4ee90f82a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -34,10 +34,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(event: TimelineEvent, highlight: Boolean, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null, readMarkerVisible) + val informationData = informationDataFactory.create(event, null) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 618ca121c2..5b6dec9900 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -33,14 +33,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(event: TimelineEvent, nextEvent: TimelineEvent?, eventIdToHighlight: String?, - readMarkerVisible: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { when (event.root.getClearType()) { EventType.STICKER, - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -53,21 +52,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + messageItemFactory.create(event, nextEvent, highlight, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + encryptedItemFactory.create(event, nextEvent, highlight, callback) } } // Unhandled event types (yet) - EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback) + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -75,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, readMarkerVisible, callback, e) + defaultItemFactory.create(event, highlight, callback, e) } return (computedModel ?: EmptyItem_()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index e44e657733..34e34fc7de 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -39,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData { + fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -63,8 +63,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = readMarkerVisible && event.hasReadMarker - return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -88,9 +86,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } - .toList(), - hasReadMarker = event.hasReadMarker, - displayReadMarker = displayReadMarker + .toList() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index c2aaf482ae..7efbce0073 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -21,6 +21,19 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) + : VectorEpoxyModel.OnVisibilityStateChangedListener { + + private var dispatched: Boolean = false + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE && !dispatched) { + dispatched = true + callback?.onReadMarkerDisplayed() + } + } +} + class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, private val event: TimelineEvent) : VectorEpoxyModel.OnVisibilityStateChangedListener { 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 2ca6bbfd37..713b60d4d8 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 @@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -50,13 +49,6 @@ abstract class AbsMessageItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) - } - } - var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) @@ -110,12 +102,6 @@ abstract class AbsMessageItem : BaseEventItem() { attributes.avatarRenderer, _readReceiptsClickListener ) - holder.readMarkerView.bindView( - attributes.informationData.eventId, - attributes.informationData.hasReadMarker, - attributes.informationData.displayReadMarker, - _readMarkerCallback - ) val reactions = attributes.informationData.orderedReactionList if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { @@ -138,7 +124,6 @@ abstract class AbsMessageItem : BaseEventItem() { } override fun unbind(holder: H) { - holder.readMarkerView.unbind() holder.readReceiptsView.unbind() super.unbind(holder) } 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 8543484b00..576e596f90 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 @@ -26,7 +26,6 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DimensionConverter @@ -62,7 +61,6 @@ abstract class BaseEventItem : VectorEpoxyModel val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) override fun bindView(itemView: View) { super.bindView(itemView) 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 01e82ddf6b..bbccb71ffd 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 @@ -25,7 +25,6 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -39,13 +38,6 @@ abstract class MergedHeaderItem : BaseEventItem() { attributes.mergeData.distinctBy { it.userId } } - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed) - } - } - override fun getViewType() = STUB_ID override fun bind(holder: Holder) { @@ -77,16 +69,6 @@ abstract class MergedHeaderItem : BaseEventItem() { } // No read receipt for this item holder.readReceiptsView.isVisible = false - holder.readMarkerView.bindView( - attributes.readMarkerId, - !attributes.readMarkerId.isNullOrEmpty(), - attributes.showReadMarker, - _readMarkerCallback) - } - - override fun unbind(holder: Holder) { - holder.readMarkerView.unbind() - super.unbind(holder) } override fun getEventIds(): List { @@ -102,9 +84,7 @@ abstract class MergedHeaderItem : BaseEventItem() { ) data class Attributes( - val readMarkerId: String?, val isCollapsed: Boolean, - val showReadMarker: Boolean, val mergeData: List, val avatarRenderer: AvatarRenderer, val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 96c74ccb88..2dd581ce6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -33,9 +33,7 @@ data class MessageInformationData( val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList(), - val hasReadMarker: Boolean = false, - val displayReadMarker: Boolean = false + val readReceipts: List = emptyList() ) : Parcelable @Parcelize 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 1f39ae3ca4..804990cc5c 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 @@ -22,7 +22,6 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -37,12 +36,6 @@ abstract class NoticeItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - private val _readMarkerCallback = object : ReadMarkerView.Callback { - - override fun onReadMarkerLongBound(isDisplayed: Boolean) { - attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed) - } - } override fun bind(holder: Holder) { super.bind(holder) @@ -56,17 +49,6 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView( - attributes.informationData.eventId, - attributes.informationData.hasReadMarker, - attributes.informationData.displayReadMarker, - _readMarkerCallback - ) - } - - override fun unbind(holder: Holder) { - holder.readMarkerView.unbind() - super.unbind(holder) } override fun getEventIds(): List { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.kt new file mode 100644 index 0000000000..4d867156d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/TimelineReadMarkerItem.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.features.home.room.detail.timeline.item + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_timeline_read_marker) +abstract class TimelineReadMarkerItem : VectorEpoxyModel() { + + override fun bind(holder: Holder) { + } + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 50ed0aae23..ce47847550 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -11,7 +11,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignBottom="@+id/readMarkerView" + android:layout_alignBottom="@+id/informationBottom" android:layout_alignParentTop="true" android:background="?riotx_highlighted_message_background" /> @@ -145,15 +145,4 @@ - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 583997577a..b72933b94f 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -53,31 +53,13 @@ - - - - - - - + android:layout_alignParentEnd="true" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml new file mode 100644 index 0000000000..93150b45ea --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file From ab489df83dd0d90936473cc889455a45e02062ea Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 19 Nov 2019 22:23:27 +0100 Subject: [PATCH 02/13] Read marker: don't show unread on events we own --- .../timeline/TimelineEventController.kt | 61 ++++++++++++------- .../res/layout/item_timeline_read_marker.xml | 4 +- 2 files changed, 41 insertions(+), 24 deletions(-) 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 9614d2aba7..180deb998b 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 @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -39,9 +40,11 @@ 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 +import timber.log.Timber import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, + private val session: Session, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, @@ -239,21 +242,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun getModels(): List> { - synchronized(modelCache) { - val readMarkerIdSnapshot = this.readMarkerIdSnapshot - val displayableReadMarkerId = if (readMarkerIdSnapshot != null) { - timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) - } else { - null - } - (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains additional models - // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.hasAdditionalModel() == true) { - modelCache[position] = buildItemModels(position, currentSnapshot, displayableReadMarkerId) - } - } - } + buildCacheItemsIfNeeded() return modelCache .map { val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { @@ -261,13 +250,41 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(it?.readMarkerModel, eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel, it?.readMarkerModel) } .flatten() .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { + private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { + if (modelCache.isEmpty()) { + return + } + val displayableReadMarkerId = computeDisplayableReadMarkerId() + (0 until modelCache.size).forEach { position -> + // Should be build if not cached or if cached but contains additional models + // We then are sure we always have items up to date. + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot, displayableReadMarkerId) + } + } + } + + private fun computeDisplayableReadMarkerId(): String? { + val readMarkerIdSnapshot = this.readMarkerIdSnapshot ?: return null + val firstDisplayableEventId = timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) ?: return null + val firstDisplayableEventIndex = timeline?.getIndexOfEvent(firstDisplayableEventId) ?: return null + for (i in (firstDisplayableEventIndex - 1) downTo 0) { + val timelineEvent = currentSnapshot.getOrNull(i) ?: return null + val isFromMe = timelineEvent.root.senderId == session.myUserId + if (!isFromMe) { + return timelineEvent.root.eventId + } + } + return null + } + + private fun buildCacheItem(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -288,15 +305,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(currentPosition, event, displayableReadMarkerId) + val readMarkerItem = buildReadMarkerItem(event, displayableReadMarkerId) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) } - private fun buildReadMarkerItem(currentPosition: Int, event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { - return if (currentPosition != 0 && event.root.eventId == displayableReadMarkerId) { + private fun buildReadMarkerItem(event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { + return if (event.root.eventId == displayableReadMarkerId) { TimelineReadMarkerItem_() .also { - it.id(event.localId) + it.id("read_marker") it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) } } else { @@ -347,6 +364,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun hasAdditionalModel() = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null + fun shouldTriggerBuild() = mergedHeaderModel != null || formattedDayModel != null } } diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 93150b45ea..8ee56045bf 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -7,8 +7,8 @@ Date: Wed, 20 Nov 2019 16:13:11 +0100 Subject: [PATCH 03/13] Read marker: continue rework [WIP] --- .../api/session/room/timeline/Timeline.kt | 9 +- .../session/room/timeline/DefaultTimeline.kt | 41 +++-- .../core/ui/views/JumpToReadMarkerView.kt | 13 +- .../home/room/detail/ReadMarkerHelper.kt | 64 -------- .../home/room/detail/RoomDetailFragment.kt | 49 ++++-- .../home/room/detail/RoomDetailViewModel.kt | 144 ++++++++++++------ .../home/room/detail/RoomDetailViewState.kt | 11 +- .../timeline/TimelineEventController.kt | 99 ++++++------ ...imelineVisibilityStateChangedListeners.kt} | 9 +- 9 files changed, 229 insertions(+), 210 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/{TimelineEventVisibilityStateChangedListener.kt => TimelineVisibilityStateChangedListeners.kt} (91%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index c03effd7ad..a71f5b7479 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline */ interface Timeline { - var listener: Listener? + val timelineID: String val isLive: Boolean + fun addListener(listener: Listener): Boolean + + fun removeListener(listener: Listener): Boolean + + fun removeAllListeners() + /** * This should be called before any other method after creating the timeline. It ensures the underlying database is open */ @@ -116,4 +122,5 @@ interface Timeline { */ BACKWARDS } + } 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 aa4bd42bf7..4411a039a5 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 @@ -81,14 +81,7 @@ internal class DefaultTimeline( val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } - override var listener: Timeline.Listener? = null - set(value) { - field = value - BACKGROUND_HANDLER.post { - postSnapshot() - } - } - + private val listeners = ArrayList() private val isStarted = AtomicBoolean(false) private val isReady = AtomicBoolean(false) private val mainHandler = createUIHandler() @@ -109,7 +102,7 @@ internal class DefaultTimeline( private val backwardsState = AtomicReference(State()) private val forwardsState = AtomicReference(State()) - private val timelineID = UUID.randomUUID().toString() + override val timelineID = UUID.randomUUID().toString() override val isLive get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) @@ -295,6 +288,20 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } + override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + listeners.add(listener).also { + postSnapshot() + } + } + + override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { + listeners.remove(listener) + } + + override fun removeAllListeners() = synchronized(listeners) { + listeners.clear() + } + // TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { @@ -487,9 +494,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 @@ -564,7 +571,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) } } @@ -637,7 +644,13 @@ internal class DefaultTimeline( } updateLoadingStates(filteredEvents) val snapshot = createSnapshot() - val runnable = Runnable { listener?.onUpdated(snapshot) } + val runnable = Runnable { + synchronized(listeners) { + listeners.forEach { + it.onUpdated(snapshot) + } + } + } debouncer.debounce("post_snapshot", runnable, 50) } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index abc2dd98f8..5ba482837e 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -34,7 +34,7 @@ class JumpToReadMarkerView @JvmOverloads constructor( ) : RelativeLayout(context, attrs, defStyleAttr) { interface Callback { - fun onJumpToReadMarkerClicked(readMarkerId: String) + fun onJumpToReadMarkerClicked() fun onClearReadMarkerClicked() } @@ -44,24 +44,15 @@ class JumpToReadMarkerView @JvmOverloads constructor( setupView() } - private var readMarkerId: String? = null - private fun setupView() { inflate(context, R.layout.view_jump_to_read_marker, this) setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) jumpToReadMarkerLabelView.setOnClickListener { - readMarkerId?.also { - callback?.onJumpToReadMarkerClicked(it) - } + callback?.onJumpToReadMarkerClicked() } closeJumpToReadMarkerView.setOnClickListener { visibility = View.INVISIBLE callback?.onClearReadMarkerClicked() } } - - fun render(show: Boolean, readMarkerId: String?) { - this.readMarkerId = readMarkerId - isInvisible = !show - } } 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 deleted file mode 100644 index 98556cc7fa..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - - * 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.features.home.room.detail - -import androidx.recyclerview.widget.LinearLayoutManager -import im.vector.riotx.core.di.ScreenScope -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import javax.inject.Inject - -@ScreenScope -class ReadMarkerHelper @Inject constructor() { - - lateinit var timelineEventController: TimelineEventController - lateinit var layoutManager: LinearLayoutManager - var callback: Callback? = null - private var jumpToReadMarkerVisible = false - private var state: RoomDetailViewState? = null - - fun updateWith(newState: RoomDetailViewState) { - state = newState - checkJumpToReadMarkerVisibility() - } - - private fun checkJumpToReadMarkerVisibility() { - val nonNullState = this.state ?: return - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId - val newJumpToReadMarkerVisible = if (readMarkerId == null) { - false - } else { - val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId) - ?: readMarkerId - val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId) - if (positionOfReadMarker == null) { - nonNullState.timeline?.isLive == true && lastVisibleItem > 0 - } else { - positionOfReadMarker > lastVisibleItem - } - } - if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) { - jumpToReadMarkerVisible = newJumpToReadMarkerVisible - callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId) - } - } - - interface Callback { - fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) - } -} 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 e0c43b9e74..8fb4d30b9f 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 @@ -39,6 +39,7 @@ import androidx.core.text.buildSpannedString import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -57,7 +58,6 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState @@ -145,8 +145,7 @@ class RoomDetailFragment @Inject constructor( val textComposerViewModelFactory: TextComposerViewModel.Factory, private val errorFormatter: ErrorFormatter, private val eventHtmlRenderer: EventHtmlRenderer, - private val vectorPreferences: VectorPreferences, - private val readMarkerHelper: ReadMarkerHelper + private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -425,7 +424,8 @@ class RoomDetailFragment @Inject constructor( if (text != composerLayout.composerEditText.text.toString()) { // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length + ?: 0) } } @@ -474,13 +474,7 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) - } - readMarkerHelper.timelineEventController = timelineEventController - readMarkerHelper.layoutManager = layoutManager - readMarkerHelper.callback = object : ReadMarkerHelper.Callback { - override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) { - jumpToReadMarkerView.render(show, readMarkerId) - } + checkJumpToUnreadBanner() } recyclerView.adapter = timelineEventController.adapter @@ -526,6 +520,25 @@ class RoomDetailFragment @Inject constructor( } } + private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post { + withState(roomDetailViewModel) { + val showJumpToUnreadBanner = when (it.unreadState) { + UnreadState.Unknown, + UnreadState.HasNoUnread -> false + is UnreadState.HasUnread -> { + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() + if (positionOfReadMarker == null) { + it.timeline?.isLive == true && lastVisibleItem > 0 + } else { + positionOfReadMarker > lastVisibleItem + } + } + } + jumpToReadMarkerView.isVisible = showJumpToUnreadBanner + } + } + private fun updateJumpToBottomViewVisibility() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") @@ -656,7 +669,6 @@ class RoomDetailFragment @Inject constructor( } private fun renderState(state: RoomDetailViewState) { - readMarkerHelper.updateWith(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() @@ -1018,10 +1030,15 @@ class RoomDetailFragment @Inject constructor( .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerDisplayed() { + override fun onReadMarkerVisible() { + checkJumpToUnreadBanner() roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + override fun onReadMarkerInvisible() { + checkJumpToUnreadBanner() + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { @@ -1226,8 +1243,10 @@ class RoomDetailFragment @Inject constructor( // JumpToReadMarkerView.Callback - override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false)) + override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { + if (it.unreadState is UnreadState.HasUnread) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false)) + } } override fun onClearReadMarkerClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index e1ff991797..ccce70f33c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.* import com.jakewharton.rxrelay2.BehaviorRelay +import com.jakewharton.rxrelay2.PublishRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -35,11 +36,13 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent @@ -59,6 +62,8 @@ import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences +import io.reactivex.Observable +import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -72,7 +77,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState), Timeline.Listener { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -80,18 +85,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, - filterEdits = false, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = false, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, - filterEdits = true, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = true, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } + private var timelineEvents = PublishRelay.create>() private var timeline = room.createTimeline(eventId, timelineSettings) // Can be used for several actions, for a one shot result @@ -125,13 +131,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { - getSnapshotOfReadMarkerId() + getUnreadState() observeSyncState() observeRoomSummary() observeEventDisplayedActions() observeSummaryState() observeDrafts() + observeUnreadState() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() + timeline.addListener(this) timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -164,16 +172,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailAction.ReportContent -> handleReportContent(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) - is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> handleExitTrackingUnreadMessages() + is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() } } - private fun handleEnterTrackingUnreadMessages() { + private fun startTrackingUnreadMessages() { trackUnreadMessages.set(true) } - private fun handleExitTrackingUnreadMessages() { + private fun stopTrackingUnreadMessages() { if (trackUnreadMessages.getAndSet(false)) { mostRecentDisplayedEvent?.root?.eventId?.also { room.setReadMarker(it, callback = object : MatrixCallback {}) @@ -212,23 +220,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -237,7 +245,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -375,7 +383,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -384,13 +392,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -401,7 +409,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) @@ -517,7 +525,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name - ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) } } } @@ -647,6 +655,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { + stopTrackingUnreadMessages() val targetEventId: String = action.eventId val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) @@ -705,7 +714,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy + val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event + ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -775,19 +785,53 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun getSnapshotOfReadMarkerId() { - room.rx().liveRoomSummary() - .unwrap() - .filter { it.readMarkerId != null } - .take(1) - .subscribe { roomSummary -> + private fun getUnreadState() { + Observable + .combineLatest, RoomSummary, UnreadState>( + timelineEvents, + room.rx().liveRoomSummary().unwrap(), + BiFunction { timelineEvents, roomSummary -> + computeUnreadState(timelineEvents, roomSummary) + } + ) + .takeUntil { + it != UnreadState.Unknown + } + .subscribe { unreadState -> setState { - copy(readMarkerIdSnapshot = roomSummary.readMarkerId) + copy(unreadState = unreadState) } } .disposeOnClear() } + private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown + val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) + ?: return UnreadState.Unknown + val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) + ?: return UnreadState.Unknown + for (i in (firstDisplayableEventIndex - 1) downTo 0) { + val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown + val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown + val isFromMe = timelineEvent.root.senderId == session.myUserId + if (!isFromMe) { + return UnreadState.HasUnread(eventId) + } + } + return UnreadState.HasNoUnread + } + + + private fun observeUnreadState() { + selectSubscribe(RoomDetailViewState::unreadState) { + Timber.v("Unread state: $it") + if (it is UnreadState.HasNoUnread) { + startTrackingUnreadMessages() + } + } + } + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { @@ -803,8 +847,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + override fun onUpdated(snapshot: List) { + timelineEvents.accept(snapshot) + setState { copy(currentSnapshot = snapshot) } + } + override fun onCleared() { timeline.dispose() + timeline.removeAllListeners() super.onCleared() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index e476545aa8..23971a93cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -41,6 +41,12 @@ sealed class SendMode(open val text: String) { data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) } +sealed class UnreadState { + object Unknown : UnreadState() + object HasNoUnread : UnreadState() + data class HasUnread(val eventId: String) : UnreadState() +} + data class RoomDetailViewState( val roomId: String, val eventId: String?, @@ -53,7 +59,10 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val readMarkerIdSnapshot: String? = null + val currentSnapshot: List = emptyList(), + val hasMoreToLoadForward: Boolean = false, + val hasMoreToLoadBackward: Boolean = false, + val unreadState: UnreadState = UnreadState.Unknown ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) 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 180deb998b..d4447f5b05 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 @@ -33,6 +33,7 @@ import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState +import im.vector.riotx.features.home.room.detail.UnreadState 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.* @@ -40,7 +41,6 @@ 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 -import timber.log.Timber import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, @@ -82,7 +82,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerDisplayed() + fun onReadMarkerVisible() + fun onReadMarkerInvisible() } interface UrlClickCallback { @@ -97,7 +98,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null - private var readMarkerIdSnapshot: String? = null + private var unreadState: UnreadState = UnreadState.Unknown + private var positionOfReadMarker: Int? = null + private var eventIdToHighlight: String? = null var callback: Callback? = null @@ -150,6 +153,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Update position when we are building new items override fun intercept(models: MutableList>) { + positionOfReadMarker = null adapterPositionMapping.clear() models.forEachIndexed { index, epoxyModel -> if (epoxyModel is BaseEventItem) { @@ -157,19 +161,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec adapterPositionMapping[it] = index } } + if (epoxyModel is TimelineReadMarkerItem) { + positionOfReadMarker = index + } } } fun update(viewState: RoomDetailViewState) { - if (timeline != viewState.timeline) { + if (timeline?.timelineID != viewState.timeline?.timelineID) { timeline = viewState.timeline - timeline?.listener = this - // Clear cache - synchronized(modelCache) { - for (i in 0 until modelCache.size) { - modelCache[i] = null - } - } + timeline?.addListener(this) } var requestModelBuild = false if (eventIdToHighlight != viewState.highlightedEventId) { @@ -177,7 +178,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 } } @@ -185,8 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) { - this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot + if (this.unreadState != viewState.unreadState) { + this.unreadState = viewState.unreadState requestModelBuild = true } if (requestModelBuild) { @@ -194,8 +195,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var eventIdToHighlight: String? = null - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) timelineMediaSizeProvider.recyclerView = recyclerView @@ -250,7 +249,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel, it?.readMarkerModel) + listOf(eventModel, it?.mergedHeaderModel, it?.readMarkerModel, it?.formattedDayModel) } .flatten() .filterNotNull() @@ -260,31 +259,17 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - val displayableReadMarkerId = computeDisplayableReadMarkerId() + val currentUnreadState = this.unreadState (0 until modelCache.size).forEach { position -> // Should be build if not cached or if cached but contains additional models // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { - modelCache[position] = buildCacheItem(position, currentSnapshot, displayableReadMarkerId) + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild(currentUnreadState) == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot, currentUnreadState) } } } - private fun computeDisplayableReadMarkerId(): String? { - val readMarkerIdSnapshot = this.readMarkerIdSnapshot ?: return null - val firstDisplayableEventId = timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) ?: return null - val firstDisplayableEventIndex = timeline?.getIndexOfEvent(firstDisplayableEventId) ?: return null - for (i in (firstDisplayableEventIndex - 1) downTo 0) { - val timelineEvent = currentSnapshot.getOrNull(i) ?: return null - val isFromMe = timelineEvent.root.senderId == session.myUserId - if (!isFromMe) { - return timelineEvent.root.eventId - } - } - return null - } - - private fun buildCacheItem(currentPosition: Int, items: List, displayableReadMarkerId: String?): CacheItemData { + private fun buildCacheItem(currentPosition: Int, items: List, currentUnreadState: UnreadState): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -295,29 +280,35 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(event, displayableReadMarkerId) + val readMarkerItem = buildReadMarkerItem(event, currentUnreadState) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) } - private fun buildReadMarkerItem(event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? { - return if (event.root.eventId == displayableReadMarkerId) { - TimelineReadMarkerItem_() - .also { - it.id("read_marker") - it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) - } - } else { - null + private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { + return when (currentUnreadState) { + is UnreadState.HasUnread -> { + if (event.root.eventId == currentUnreadState.eventId) { + TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + } else { + null + } + } + UnreadState.Unknown, + UnreadState.HasNoUnread -> null } } @@ -354,6 +345,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return adapterPositionMapping[eventId] } + fun getPositionOfReadMarker(): Int? = synchronized(modelCache) { + return positionOfReadMarker + } + fun isLoadingForward() = showingForwardLoader private data class CacheItemData( @@ -364,6 +359,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun shouldTriggerBuild() = mergedHeaderModel != null || formattedDayModel != null + fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt similarity index 91% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 7efbce0073..11b7d68923 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -24,12 +24,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?) : VectorEpoxyModel.OnVisibilityStateChangedListener { - private var dispatched: Boolean = false - override fun onVisibilityStateChanged(visibilityState: Int) { - if (visibilityState == VisibilityState.VISIBLE && !dispatched) { - dispatched = true - callback?.onReadMarkerDisplayed() + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onReadMarkerVisible() + } else if (visibilityState == VisibilityState.INVISIBLE) { + callback?.onReadMarkerInvisible() } } } From 64d73ae8e60be613ca48c568d89b579360bf37a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Nov 2019 14:42:16 +0100 Subject: [PATCH 04/13] Read marker: handle the jump to read marker --- .../timeline/DefaultGetContextOfEventTask.kt | 5 +- .../session/room/timeline/DefaultTimeline.kt | 2 +- .../room/timeline/EventContextResponse.kt | 5 +- .../home/room/detail/RoomDetailFragment.kt | 36 ++++--- .../home/room/detail/RoomDetailViewModel.kt | 94 ++++++++++--------- .../home/room/detail/RoomDetailViewState.kt | 9 +- .../timeline/TimelineEventController.kt | 27 +++--- .../helper/MessageInformationDataFactory.kt | 2 +- ...TimelineVisibilityStateChangedListeners.kt | 2 - 9 files changed, 98 insertions(+), 84 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 96e1caf71b..08d34d3056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task { - apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) + apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter) } return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } 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 4411a039a5..b83240a681 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 @@ -633,7 +633,7 @@ internal class DefaultTimeline( } private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId) + val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 4dfe3e5c45..f06697351e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -30,6 +30,7 @@ data class EventContextResponse( @Json(name = "state") override val stateEvents: List = emptyList() ) : TokenChunkEvent { - override val events: List - get() = listOf(event) + override val events: List by lazy { + eventsAfter.reversed() + listOf(event) + eventsBefore + } } 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 8fb4d30b9f..d50b0c9f68 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 @@ -474,7 +474,8 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) - checkJumpToUnreadBanner() + updateJumpToReadMarkerViewVisibility() + updateJumpToBottomViewVisibility() } recyclerView.adapter = timelineEventController.adapter @@ -520,18 +521,23 @@ class RoomDetailFragment @Inject constructor( } } - private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post { + private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { withState(roomDetailViewModel) { val showJumpToUnreadBanner = when (it.unreadState) { UnreadState.Unknown, - UnreadState.HasNoUnread -> false - is UnreadState.HasUnread -> { - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() - if (positionOfReadMarker == null) { - it.timeline?.isLive == true && lastVisibleItem > 0 + UnreadState.HasNoUnread -> false + is UnreadState.ReadMarkerNotLoaded -> true + is UnreadState.HasUnread -> { + if (it.canShowJumpToReadMarker) { + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() + if (positionOfReadMarker == null) { + false + } else { + positionOfReadMarker > lastVisibleItem + } } else { - positionOfReadMarker > lastVisibleItem + false } } } @@ -1031,14 +1037,10 @@ class RoomDetailFragment @Inject constructor( } override fun onReadMarkerVisible() { - checkJumpToUnreadBanner() + updateJumpToReadMarkerViewVisibility() roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } - override fun onReadMarkerInvisible() { - checkJumpToUnreadBanner() - } - // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { @@ -1244,8 +1246,12 @@ class RoomDetailFragment @Inject constructor( // JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { + jumpToReadMarkerView.isVisible = false if (it.unreadState is UnreadState.HasUnread) { - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false)) + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false)) + } + if (it.unreadState is UnreadState.ReadMarkerNotLoaded) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index ccce70f33c..2dab9264e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -65,6 +65,7 @@ import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber @@ -85,16 +86,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, - filterEdits = false, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = false, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, - filterEdits = true, - filterTypes = true, - allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) + filterEdits = true, + filterTypes = true, + allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } private var timelineEvents = PublishRelay.create>() @@ -179,6 +180,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun startTrackingUnreadMessages() { trackUnreadMessages.set(true) + setState { copy(canShowJumpToReadMarker = false) } } private fun stopTrackingUnreadMessages() { @@ -188,6 +190,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } mostRecentDisplayedEvent = null } + setState { copy(canShowJumpToReadMarker = true) } } private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { @@ -220,23 +223,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -245,7 +248,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -383,7 +386,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { // TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -392,13 +395,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -409,7 +412,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) @@ -525,7 +528,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { null -> room.sendMedias(attachments) else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name - ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) } } } @@ -715,7 +718,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event - ?: return@subscribeBy + ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { @@ -788,29 +791,33 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun getUnreadState() { Observable .combineLatest, RoomSummary, UnreadState>( - timelineEvents, + timelineEvents.observeOn(Schedulers.computation()), room.rx().liveRoomSummary().unwrap(), BiFunction { timelineEvents, roomSummary -> computeUnreadState(timelineEvents, roomSummary) } ) - .takeUntil { - it != UnreadState.Unknown - } - .subscribe { unreadState -> - setState { - copy(unreadState = unreadState) + // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread + .distinctUntilChanged { previous, current -> + when { + previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false + current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true + else -> false } } + .subscribe { + setState { copy(unreadState = it) } + } .disposeOnClear() } private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { + if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) - ?: return UnreadState.Unknown + ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) - ?: return UnreadState.Unknown + ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) for (i in (firstDisplayableEventIndex - 1) downTo 0) { val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown @@ -849,7 +856,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun onUpdated(snapshot: List) { timelineEvents.accept(snapshot) - setState { copy(currentSnapshot = snapshot) } } override fun onCleared() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 23971a93cd..a0be8fc9dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -44,7 +44,8 @@ sealed class SendMode(open val text: String) { sealed class UnreadState { object Unknown : UnreadState() object HasNoUnread : UnreadState() - data class HasUnread(val eventId: String) : UnreadState() + data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState() + data class HasUnread(val firstUnreadEventId: String) : UnreadState() } data class RoomDetailViewState( @@ -59,10 +60,8 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val currentSnapshot: List = emptyList(), - val hasMoreToLoadForward: Boolean = false, - val hasMoreToLoadBackward: Boolean = false, - val unreadState: UnreadState = UnreadState.Unknown + val unreadState: UnreadState = UnreadState.Unknown, + val canShowJumpToReadMarker: Boolean = true ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) 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 d4447f5b05..e3e9cf7378 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 @@ -83,7 +83,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) fun onReadMarkerVisible() - fun onReadMarkerInvisible() } interface UrlClickCallback { @@ -178,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 } } @@ -280,12 +279,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } val mergedHeaderModel = mergedHeaderItemFactory.create(event, - nextEvent = nextEvent, - items = items, - addDaySeparator = addDaySeparator, - currentPosition = currentPosition, - eventIdToHighlight = eventIdToHighlight, - callback = callback + nextEvent = nextEvent, + items = items, + addDaySeparator = addDaySeparator, + currentPosition = currentPosition, + eventIdToHighlight = eventIdToHighlight, + callback = callback ) { requestModelBuild() } @@ -297,7 +296,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { return when (currentUnreadState) { is UnreadState.HasUnread -> { - if (event.root.eventId == currentUnreadState.eventId) { + if (event.root.eventId == currentUnreadState.firstUnreadEventId) { TimelineReadMarkerItem_() .also { it.id("read_marker") @@ -307,8 +306,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec null } } - UnreadState.Unknown, - UnreadState.HasNoUnread -> null + else -> null } } @@ -359,6 +357,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val formattedDayModel: DaySeparatorItem? = null, val readMarkerModel: TimelineReadMarkerItem? = null ) { - fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId) + fun shouldTriggerBuild(unreadState: UnreadState): Boolean { + return mergedHeaderModel != null + || formattedDayModel != null + || readMarkerModel != null + || (unreadState is UnreadState.HasUnread && unreadState.firstUnreadEventId == eventId) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 34e34fc7de..784a180d00 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -47,7 +47,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false + ?: false val showInformation = addDaySeparator diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 11b7d68923..69b2b24899 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -27,8 +27,6 @@ class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEve override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { callback?.onReadMarkerVisible() - } else if (visibilityState == VisibilityState.INVISIBLE) { - callback?.onReadMarkerInvisible() } } } From bba52e77d1be6d857e2596120e90d6fe57a3e86d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Nov 2019 16:25:55 +0100 Subject: [PATCH 05/13] Read marker: fix merged items --- .../timeline/TimelineEventController.kt | 58 ++++++++----------- .../detail/timeline/item/MergedHeaderItem.kt | 6 +- 2 files changed, 28 insertions(+), 36 deletions(-) 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 e3e9cf7378..503e01e5f8 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 @@ -151,7 +151,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } // Update position when we are building new items - override fun intercept(models: MutableList>) { + override fun intercept(models: MutableList>) = synchronized(modelCache) { positionOfReadMarker = null adapterPositionMapping.clear() models.forEachIndexed { index, epoxyModel -> @@ -160,8 +160,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec adapterPositionMapping[it] = index } } - if (epoxyModel is TimelineReadMarkerItem) { - positionOfReadMarker = index + } + val currentUnreadState = this.unreadState + if (currentUnreadState is UnreadState.HasUnread) { + val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1) + positionOfReadMarker = position + if (position != null) { + val readMarker = TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + models.add(position, readMarker) } } } @@ -218,7 +228,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - // Timeline.LISTENER *************************************************************************** +// Timeline.LISTENER *************************************************************************** override fun onUpdated(snapshot: List) { submitSnapshot(snapshot) @@ -248,7 +258,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { it.eventModel } - listOf(eventModel, it?.mergedHeaderModel, it?.readMarkerModel, it?.formattedDayModel) + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) } .flatten() .filterNotNull() @@ -258,17 +268,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - val currentUnreadState = this.unreadState (0 until modelCache.size).forEach { position -> // Should be build if not cached or if cached but contains additional models // We then are sure we always have items up to date. - if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild(currentUnreadState) == true) { - modelCache[position] = buildCacheItem(position, currentSnapshot, currentUnreadState) + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { + modelCache[position] = buildCacheItem(position, currentSnapshot) } } } - private fun buildCacheItem(currentPosition: Int, items: List, currentUnreadState: UnreadState): CacheItemData { + private fun buildCacheItem(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() @@ -289,25 +298,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - val readMarkerItem = buildReadMarkerItem(event, currentUnreadState) - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem) - } - - private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { - return when (currentUnreadState) { - is UnreadState.HasUnread -> { - if (event.root.eventId == currentUnreadState.firstUnreadEventId) { - TimelineReadMarkerItem_() - .also { - it.id("read_marker") - it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) - } - } else { - null - } - } - else -> null - } + return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) } private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { @@ -354,14 +345,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventId: String?, val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null, - val readMarkerModel: TimelineReadMarkerItem? = null + val formattedDayModel: DaySeparatorItem? = null ) { - fun shouldTriggerBuild(unreadState: UnreadState): Boolean { - return mergedHeaderModel != null - || formattedDayModel != null - || readMarkerModel != null - || (unreadState is UnreadState.HasUnread && unreadState.firstUnreadEventId == eventId) + fun shouldTriggerBuild(): Boolean { + return mergedHeaderModel != null || formattedDayModel != null + } } } 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 bbccb71ffd..3ffc8e65d6 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 @@ -72,7 +72,11 @@ abstract class MergedHeaderItem : BaseEventItem() { } override fun getEventIds(): List { - return attributes.mergeData.map { it.eventId } + return if (attributes.isCollapsed) { + attributes.mergeData.map { it.eventId } + } else { + emptyList() + } } data class Data( From 8e873672a9a4108f595393194998cb7ce5a8fc33 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:17:23 +0100 Subject: [PATCH 06/13] Read marker: change design --- .../res/layout/item_timeline_read_marker.xml | 53 +++++++++++++++---- vector/src/main/res/values/strings_riotX.xml | 1 + 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 8ee56045bf..8b4fa261d9 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -1,18 +1,51 @@ - - + android:layout_height="wrap_content" + android:background="?riotx_background" + android:padding="8dp"> + + + + + + android:background="@color/notification_accent_color" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/itemDayTextView" + app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index dd8dd52dc6..f259a34e44 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,6 +22,7 @@ %1$s made the room public to whoever knows the link. %1$s made the room invite only. + Unread messages Liberate your communication Chat with people directly or in groups From 90c472fef9c74a3924c60f53f32543167011f635 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:17:54 +0100 Subject: [PATCH 07/13] Read marker: fix mark all as read --- .../database/query/ReadMarkerEntityQueries.kt | 8 ++---- .../internal/database/query/ReadQueries.kt | 27 +++++++++++++++++-- .../session/room/read/SetReadMarkersTask.kt | 24 +++-------------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt index 061634a9da..d95dc58574 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt @@ -22,13 +22,9 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where -internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery { - val query = realm.where() +internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { + return realm.where() .equalTo(ReadMarkerEntityFields.ROOM_ID, roomId) - if (eventId != null) { - query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId) - } - return query } internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index 0a925ac1ab..f1045cdb36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import io.realm.Realm internal fun isEventRead(monarchy: Monarchy, userId: String?, @@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy, isEventRead = if (eventToCheck?.sender == userId) { true } else { - val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() + ?: return@doWithRealm + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex + ?: Int.MIN_VALUE val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex @@ -49,3 +53,22 @@ internal fun isEventRead(monarchy: Monarchy, return isEventRead } + +internal fun isReadMarkerMoreRecent(monarchy: Monarchy, + roomId: String?, + eventId: String?): Boolean { + if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false + val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root + + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false + val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex + ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + eventToCheckIndex <= readMarkerIndex + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 7e5de176bb..83d12d0bae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -57,22 +57,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = HashMap() - val fullyReadEventId: String? - val readReceiptEventId: String? Timber.v("Execute set read marker with params: $params") - if (params.markAllAsRead) { + val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) { val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId } - fullyReadEventId = latestSyncedEventId - readReceiptEventId = latestSyncedEventId + Pair(latestSyncedEventId, latestSyncedEventId) } else { - fullyReadEventId = params.fullyReadEventId - readReceiptEventId = params.readReceiptEventId + Pair(params.fullyReadEventId, params.readReceiptEventId) } - if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { + if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event $fullyReadEventId") } else { @@ -118,16 +114,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } } - - private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId - ?: return true - val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst() - val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst() - val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE - val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE - newReadMarkerIndex > currentReadMarkerIndex - } - } } From 0376de08f42d774abc8f90fc438f7bf0e69691d1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:30:54 +0100 Subject: [PATCH 08/13] Clean files --- .../vector/matrix/android/api/session/room/timeline/Timeline.kt | 1 - .../vector/matrix/android/internal/database/query/ReadQueries.kt | 1 - .../android/internal/session/room/read/SetReadMarkersTask.kt | 1 - .../java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt | 1 - .../riotx/features/home/room/detail/RoomDetailViewModel.kt | 1 - .../home/room/detail/timeline/TimelineEventController.kt | 1 - .../riotx/features/home/room/detail/timeline/item/NoticeItem.kt | 1 - 7 files changed, 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index a71f5b7479..b55c830c43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -122,5 +122,4 @@ interface Timeline { */ BACKWARDS } - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index f1045cdb36..c214886ec8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -71,4 +71,3 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy, eventToCheckIndex <= readMarkerIndex } } - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 83d12d0bae..b9dca748cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index 5ba482837e..b2adde449a 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -23,7 +23,6 @@ import android.util.AttributeSet import android.view.View import android.widget.RelativeLayout import androidx.core.content.ContextCompat -import androidx.core.view.isInvisible import im.vector.riotx.R import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 2dab9264e5..642bce3319 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -829,7 +829,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro return UnreadState.HasNoUnread } - private fun observeUnreadState() { selectSubscribe(RoomDetailViewState::unreadState) { Timber.v("Unread state: $it") 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 503e01e5f8..326e19c431 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 @@ -349,7 +349,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec ) { fun shouldTriggerBuild(): Boolean { return mergedHeaderModel != null || formattedDayModel != null - } } } 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 804990cc5c..05dedcfa22 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 @@ -36,7 +36,6 @@ abstract class NoticeItem : BaseEventItem() { attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) - override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = attributes.noticeText From 7890b929a7843b1602a20006b2b5ea9f3b2af5f8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 22 Nov 2019 12:32:45 +0100 Subject: [PATCH 09/13] Update CHANGES --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 3ead09faac..e1302bc957 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Features ✨: Improvements 🙌: - Send mention Pills from composer - Links in message preview in the bottom sheet are now active. + - Rework the read marker to make it more usable Other changes: - Fix a small grammatical error when an empty room list is shown. From e7a47ae32ae1fa64437439ff6483c5d763c30cf8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 27 Nov 2019 18:52:22 +0100 Subject: [PATCH 10/13] Some cleanup --- .../android/api/session/room/timeline/Timeline.kt | 2 +- .../main/res/layout/item_timeline_read_marker.xml | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index b55c830c43..85dbdcaa19 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -104,7 +104,7 @@ interface Timeline { interface Listener { /** * Call when the timeline has been updated through pagination or sync. - * @param snapshot the most uptodate snapshot + * @param snapshot the most up to date snapshot */ fun onUpdated(snapshot: List) } diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index 8b4fa261d9..fdc4fc198d 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -7,7 +7,7 @@ android:padding="8dp"> - From 9510d71cd375b437428a46ddae4f0facf0567a64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 27 Nov 2019 18:53:29 +0100 Subject: [PATCH 11/13] Proposal for simple layout --- .../layout/item_timeline_read_marker_bma.xml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 vector/src/main/res/layout/item_timeline_read_marker_bma.xml diff --git a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml new file mode 100644 index 0000000000..1a8be7bdf1 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file From f9eb80b4ece3910291aac6aedc7271c97ed0306e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 11:32:57 +0100 Subject: [PATCH 12/13] Simplify layout --- .../item_timeline_event_day_separator.xml | 48 ++++--------------- .../res/layout/item_timeline_read_marker.xml | 43 +++++------------ .../layout/item_timeline_read_marker_bma.xml | 28 ----------- 3 files changed, 21 insertions(+), 98 deletions(-) delete mode 100644 vector/src/main/res/layout/item_timeline_read_marker_bma.xml diff --git a/vector/src/main/res/layout/item_timeline_event_day_separator.xml b/vector/src/main/res/layout/item_timeline_event_day_separator.xml index 81e94fd68e..13b70c4243 100644 --- a/vector/src/main/res/layout/item_timeline_event_day_separator.xml +++ b/vector/src/main/res/layout/item_timeline_event_day_separator.xml @@ -1,6 +1,5 @@ - - + android:layout_marginEnd="8dp" + android:background="?riotx_header_panel_background" /> - - - - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_read_marker.xml b/vector/src/main/res/layout/item_timeline_read_marker.xml index fdc4fc198d..e76ffa3d5c 100644 --- a/vector/src/main/res/layout/item_timeline_read_marker.xml +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -1,49 +1,28 @@ - + android:background="@color/notification_accent_color" /> + android:textSize="15sp" /> - - - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml b/vector/src/main/res/layout/item_timeline_read_marker_bma.xml deleted file mode 100644 index 1a8be7bdf1..0000000000 --- a/vector/src/main/res/layout/item_timeline_read_marker_bma.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file From a6f8fe9317a4d96ec05991cf0efc9654a6c2344f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Dec 2019 12:08:18 +0100 Subject: [PATCH 13/13] Fix lint issue --- .../features/home/room/detail/timeline/item/BaseEventItem.kt | 1 - .../home/room/detail/timeline/item/MergedHeaderItem.kt | 2 +- .../src/main/res/layout/item_timeline_event_base_noinfo.xml | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) 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 576e596f90..02b7341c72 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 @@ -19,7 +19,6 @@ import android.view.View import android.view.ViewStub import android.widget.RelativeLayout import androidx.annotation.IdRes -import androidx.core.view.marginStart import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotx.R 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 3ffc8e65d6..a2a3c9ad3b 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 @@ -103,6 +103,6 @@ abstract class MergedHeaderItem : BaseEventItem() { } companion object { - private const val STUB_ID = R.id.messageContentMergedheaderStub + private const val STUB_ID = R.id.messageContentMergedHeaderStub } } diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index b72933b94f..c1987dccb2 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -10,7 +10,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignBottom="@+id/informationBottom" + android:layout_alignBottom="@+id/readReceiptsView" android:layout_alignParentTop="true" android:background="?riotx_highlighted_message_background" /> @@ -47,7 +47,7 @@ android:layout="@layout/item_timeline_event_blank_stub" />