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. 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..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 @@ -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 */ @@ -98,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/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/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..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 @@ -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,21 @@ 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..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.* @@ -57,22 +56,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 +113,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 - } - } } 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 4127e43540..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 @@ -74,22 +74,14 @@ 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") } - 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() @@ -110,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) @@ -197,7 +189,6 @@ internal class DefaultTimeline( if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } - hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this) isReady.set(true) } } @@ -217,7 +208,6 @@ internal class DefaultTimeline( if (this::filteredEvents.isInitialized) { filteredEvents.removeAllChangeListeners() } - hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } @@ -298,7 +288,21 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } -// TimelineHiddenReadReceipts.Delegate + 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 { return rebuildEvent(eventId) { te -> @@ -310,19 +314,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 -> @@ -502,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 @@ -579,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) } } @@ -641,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) } @@ -652,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/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/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/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/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt index abc2dd98f8..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.* @@ -34,7 +33,7 @@ class JumpToReadMarkerView @JvmOverloads constructor( ) : RelativeLayout(context, attrs, defStyleAttr) { interface Callback { - fun onJumpToReadMarkerClicked(readMarkerId: String) + fun onJumpToReadMarkerClicked() fun onClearReadMarkerClicked() } @@ -44,24 +43,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/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 deleted file mode 100644 index 7b3ebeb71c..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt +++ /dev/null @@ -1,99 +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 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() - 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/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..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 @@ -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, @@ -292,6 +291,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { + roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) debouncer.cancelAll() super.onDestroy() } @@ -299,6 +299,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) { @@ -423,12 +424,12 @@ 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) } } override fun onResume() { - readMarkerHelper.onResume() super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) } @@ -473,24 +474,12 @@ 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) - } + updateJumpToReadMarkerViewVisibility() + updateJumpToBottomViewVisibility() } 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 -> { @@ -532,6 +521,30 @@ class RoomDetailFragment @Inject constructor( } } + private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { + withState(roomDetailViewModel) { + val showJumpToUnreadBanner = when (it.unreadState) { + UnreadState.Unknown, + 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 { + false + } + } + } + jumpToReadMarkerView.isVisible = showJumpToUnreadBanner + } + } + private fun updateJumpToBottomViewVisibility() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") @@ -662,13 +675,12 @@ class RoomDetailFragment @Inject constructor( } private fun renderState(state: RoomDetailViewState) { - readMarkerHelper.updateWith(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() 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 +1036,9 @@ 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 onReadMarkerVisible() { + updateJumpToReadMarkerViewVisibility() + roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } // AutocompleteUserPresenter.Callback @@ -1252,8 +1245,14 @@ class RoomDetailFragment @Inject constructor( // JumpToReadMarkerView.Callback - override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false)) + override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { + jumpToReadMarkerView.isVisible = false + if (it.unreadState is UnreadState.HasUnread) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false)) + } + if (it.unreadState is UnreadState.ReadMarkerNotLoaded) { + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, 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 a264e0d06c..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 @@ -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,14 @@ 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 import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -58,19 +62,23 @@ 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 io.reactivex.schedulers.Schedulers import org.commonmark.parser.Parser 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, 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 @@ -90,6 +98,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro 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 @@ -102,6 +111,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,48 +132,67 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { + getUnreadState() observeSyncState() observeRoomSummary() observeEventDisplayedActions() observeSummaryState() observeDrafts() + observeUnreadState() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() + timeline.addListener(this) timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } 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 -> startTrackingUnreadMessages() + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() } } + private fun startTrackingUnreadMessages() { + trackUnreadMessages.set(true) + setState { copy(canShowJumpToReadMarker = false) } + } + + private fun stopTrackingUnreadMessages() { + if (trackUnreadMessages.getAndSet(false)) { + mostRecentDisplayedEvent?.root?.eventId?.also { + room.setReadMarker(it, callback = object : MatrixCallback {}) + } + mostRecentDisplayedEvent = null + } + setState { copy(canShowJumpToReadMarker = true) } + } + private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { invisibleEventsObservable.accept(action) } @@ -627,6 +658,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) @@ -685,26 +717,23 @@ 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 +788,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun getUnreadState() { + Observable + .combineLatest, RoomSummary, UnreadState>( + timelineEvents.observeOn(Schedulers.computation()), + room.rx().liveRoomSummary().unwrap(), + BiFunction { timelineEvents, roomSummary -> + computeUnreadState(timelineEvents, roomSummary) + } + ) + // 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.ReadMarkerNotLoaded(readMarkerIdSnapshot) + val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) + ?: 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 + 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) { @@ -774,8 +853,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + override fun onUpdated(snapshot: List) { + timelineEvents.accept(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 03110858a1..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 @@ -41,6 +41,13 @@ 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 ReadMarkerNotLoaded(val readMarkerId: String): UnreadState() + data class HasUnread(val firstUnreadEventId: String) : UnreadState() +} + data class RoomDetailViewState( val roomId: String, val eventId: String?, @@ -52,7 +59,9 @@ 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 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 be2f1dd7e4..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 @@ -25,21 +25,18 @@ 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 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.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.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 @@ -47,11 +44,10 @@ import org.threeten.bp.LocalDateTime 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, - private val avatarRenderer: AvatarRenderer, - private val dimensionConverter: DimensionConverter, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -86,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) + fun onReadMarkerVisible() } interface UrlClickCallback { @@ -101,6 +97,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 unreadState: UnreadState = UnreadState.Unknown + private var positionOfReadMarker: Int? = null + private var eventIdToHighlight: String? = null var callback: Callback? = null @@ -152,7 +151,8 @@ 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 -> if (epoxyModel is BaseEventItem) { @@ -161,18 +161,25 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } } + 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) + } + } } - fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) { - if (timeline != viewState.timeline) { + fun update(viewState: RoomDetailViewState) { + 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) { @@ -188,8 +195,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventIdToHighlight = viewState.highlightedEventId requestModelBuild = true } - if (this.readMarkerVisible != readMarkerVisible) { - this.readMarkerVisible = readMarkerVisible + if (this.unreadState != viewState.unreadState) { + this.unreadState = viewState.unreadState requestModelBuild = true } if (requestModelBuild) { @@ -197,9 +204,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private var readMarkerVisible: Boolean = false - private var eventIdToHighlight: String? = null - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) timelineMediaSizeProvider.recyclerView = recyclerView @@ -224,7 +228,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - // Timeline.LISTENER *************************************************************************** +// Timeline.LISTENER *************************************************************************** override fun onUpdated(snapshot: List) { submitSnapshot(snapshot) @@ -246,43 +250,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun getModels(): List> { - synchronized(modelCache) { - (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains mergedHeader or formattedDay - // We then are sure we always have items up to date. - if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { - modelCache[position] = buildItemModels(position, currentSnapshot) - } - } - return modelCache - .map { - val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { - null - } else { - it.eventModel - } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + buildCacheItemsIfNeeded() + return modelCache + .map { + val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { + null + } else { + it.eventModel } - .flatten() - .filterNotNull() + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } + .flatten() + .filterNotNull() + } + + private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { + if (modelCache.isEmpty()) { + return + } + (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) + } } } - private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { + private fun buildCacheItem(currentPosition: Int, items: List): 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 +291,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec nextEvent = nextEvent, items = items, addDaySeparator = addDaySeparator, - readMarkerVisible = readMarkerVisible, currentPosition = currentPosition, eventIdToHighlight = eventIdToHighlight, callback = callback @@ -298,7 +298,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) - return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) } @@ -335,6 +334,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( @@ -343,5 +346,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: MergedHeaderItem? = null, val formattedDayModel: DaySeparatorItem? = null - ) + ) { + fun shouldTriggerBuild(): Boolean { + return mergedHeaderModel != null || formattedDayModel != 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..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 @@ -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!! @@ -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 @@ -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/TimelineVisibilityStateChangedListeners.kt similarity index 85% 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 c2aaf482ae..69b2b24899 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 @@ -21,6 +21,16 @@ 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 { + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onReadMarkerVisible() + } + } +} + 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..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,14 +19,12 @@ 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 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 +60,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..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 @@ -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,20 +69,14 @@ 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 { - return attributes.mergeData.map { it.eventId } + return if (attributes.isCollapsed) { + attributes.mergeData.map { it.eventId } + } else { + emptyList() + } } data class Data( @@ -102,9 +88,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, @@ -119,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/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..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 @@ -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,13 +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) holder.noticeTextView.text = attributes.noticeText @@ -56,17 +48,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..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,37 +47,19 @@ android:layout="@layout/item_timeline_event_blank_stub" /> - - - - - - - + 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_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 new file mode 100644 index 0000000000..e76ffa3d5c --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_read_marker.xml @@ -0,0 +1,28 @@ + + + + + + + + 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