From 88fb9667a3e51cb90031ae8aa2ded9167974b7f7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Sep 2019 20:21:42 +0200 Subject: [PATCH] Timeline: continue fixing issues + read marker --- .../main/java/im/vector/matrix/rx/RxRoom.kt | 13 +++- .../matrix/android/api/session/room/Room.kt | 2 +- .../api/session/room/read/ReadService.kt | 17 ++++- .../session/room/timeline/TimelineService.kt | 2 +- .../matrix/android/api/util/Optional.kt | 40 ++++++++++ .../internal/session/room/DefaultRoom.kt | 2 +- .../session/room/read/DefaultReadService.kt | 22 ++++-- .../session/room/read/FullyReadContent.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 73 +++++++++++------- .../room/timeline/DefaultTimelineService.kt | 2 +- .../session/sync/RoomFullyReadHandler.kt | 3 +- .../internal/session/sync/RoomSyncHandler.kt | 2 +- .../api/pushrules/PushrulesConditionTest.kt | 4 +- .../riotx/core/ui/views/ReadMarkerView.kt | 25 ++++--- .../im/vector/riotx/core/utils/Debouncer.kt | 6 ++ .../home/room/detail/RoomDetailFragment.kt | 74 ++++++++++--------- .../home/room/detail/RoomDetailViewModel.kt | 70 ++++++++++++------ .../home/room/detail/RoomDetailViewState.kt | 3 +- .../room/detail/ScrollOnNewMessageCallback.kt | 2 +- .../timeline/TimelineEventController.kt | 24 +++--- .../timeline/factory/DefaultItemFactory.kt | 3 +- .../timeline/factory/EncryptedItemFactory.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 16 ++-- .../timeline/factory/MessageItemFactory.kt | 5 +- .../timeline/factory/NoticeItemFactory.kt | 4 +- .../timeline/factory/TimelineItemFactory.kt | 13 ++-- .../helper/MessageInformationDataFactory.kt | 5 +- .../detail/timeline/item/AbsMessageItem.kt | 12 ++- .../detail/timeline/item/MergedHeaderItem.kt | 21 ++++++ .../timeline/item/MessageInformationData.kt | 1 + .../room/detail/timeline/item/NoticeItem.kt | 13 +++- 31 files changed, 331 insertions(+), 153 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api => internal}/session/room/read/FullyReadContent.kt (92%) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 0ff0987dfe..c8cc430c65 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -21,13 +21,14 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.util.Optional import io.reactivex.Observable import io.reactivex.Single class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable { - return room.liveRoomSummary().asObservable() + return room.getRoomSummaryLive().asObservable() } fun liveRoomMemberIds(): Observable> { @@ -39,7 +40,15 @@ class RxRoom(private val room: Room) { } fun liveTimelineEvent(eventId: String): Observable { - return room.liveTimeLineEvent(eventId).asObservable() + return room.getTimeLineEventLive(eventId).asObservable() + } + + fun liveReadMarker(): Observable> { + return room.getReadMarkerLive().asObservable() + } + + fun liveReadReceipt(): Observable> { + return room.getMyReadReceiptLive().asObservable() } fun loadRoomMembersIfNeeded(): Single = Single.create { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index ec6b382f8f..9a4e0131d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -47,7 +47,7 @@ interface Room : * A live [RoomSummary] associated with the room * You can observe this summary to get dynamic data from this room. */ - fun liveRoomSummary(): LiveData + fun getRoomSummaryLive(): LiveData fun roomSummary(): RoomSummary? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index 0ff0298b44..e315224880 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.read import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. @@ -40,12 +41,24 @@ interface ReadService { */ fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + /** + * Check if an event is already read, ie. your read receipt is set on a more recent event. + */ fun isEventRead(eventId: String): Boolean /** - * Returns a nullable read marker for the room. + * Returns a live read marker id for the room. */ - fun getReadMarkerLive(): LiveData + fun getReadMarkerLive(): LiveData> + /** + * Returns a live read receipt id for the room. + */ + fun getMyReadReceiptLive(): LiveData> + + /** + * Returns a live list of read receipts for a given event + * @param eventId: the event + */ fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index fdf99bd22c..b55bc17946 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -36,5 +36,5 @@ interface TimelineService { fun getTimeLineEvent(eventId: String): TimelineEvent? - fun liveTimeLineEvent(eventId: String): LiveData + fun getTimeLineEventLive(eventId: String): LiveData } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt new file mode 100644 index 0000000000..abe2d23993 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -0,0 +1,40 @@ +/* + + * 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.api.util + +data class Optional constructor(private val value: T?) { + + fun get(): T { + return value!! + } + + fun getOrNull(): T? { + return value + } + + fun getOrElse(fn: () -> T): T { + return value ?: fn() + } + + companion object { + fun from(value: T?): Optional { + return Optional(value) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 492dd03543..10262ccebd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -53,7 +53,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, RelationService by relationService, MembershipService by roomMembersService { - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 3709521cc3..1f0bae6fea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -83,24 +84,33 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead } - override fun getReadMarkerLive(): LiveData { + override fun getReadMarkerLive(): LiveData> { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> ReadMarkerEntity.where(realm, roomId) } return Transformations.map(liveRealmData) { results -> - results.firstOrNull()?.eventId + Optional.from(results.firstOrNull()?.eventId) + } + } + + override fun getMyReadReceiptLive(): LiveData> { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadReceiptEntity.where(realm, roomId = roomId, userId = credentials.userId) + } + return Transformations.map(liveRealmData) { results -> + Optional.from(results.firstOrNull()?.eventId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt index a73b9ef5b7..6790ea658c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/FullyReadContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/FullyReadContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.room.read +package im.vector.matrix.android.internal.session.room.read import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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 26eb16b15c..beaf4eb0af 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.auth.data.Credentials -import im.vector.matrix.android.api.session.room.read.FullyReadContent 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 @@ -76,29 +75,49 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) { if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { - Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") + Timber.w("Can't set read marker for local event $fullyReadEventId") } else { - updateReadMarker(params.roomId, fullyReadEventId) markers[READ_MARKER] = fullyReadEventId } } + if (readReceiptEventId != null - && !isEventRead(params.roomId, readReceiptEventId)) { + && !isEventRead(params.roomId, readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { - Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") + Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { - updateNotificationCountIfNecessary(params.roomId, readReceiptEventId) markers[READ_RECEIPT] = readReceiptEventId } } if (markers.isEmpty()) { return } + updateDatabase(params.roomId, markers) executeRequest { apiCall = roomAPI.sendReadMarker(params.roomId, markers) } } + private suspend fun updateDatabase(roomId: String, markers: HashMap) { + monarchy.awaitTransaction { realm -> + val readMarkerId = markers[READ_MARKER] + val readReceiptId = markers[READ_RECEIPT] + + if (readMarkerId != null) { + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) + } + if (readReceiptId != null) { + val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId + if (isLatestReceived) { + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: return@awaitTransaction + roomSummary.notificationCount = 0 + roomSummary.highlightCount = 0 + } + } + } + } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> @@ -111,36 +130,36 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private suspend fun updateReadMarker(roomId: String, eventId: String) { - monarchy.awaitTransaction { realm -> - roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId)) - } - } - - private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { - monarchy.awaitTransaction { realm -> - val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId - if (isLatestReceived) { - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@awaitTransaction - roomSummary.notificationCount = 0 - roomSummary.highlightCount = 0 - } - } - } private fun isEventRead(roomId: String, eventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() - ?: return false + ?: return false val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - ?: return false + ?: return false val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE eventToCheckIndex <= readReceiptIndex } } + private fun SetReadMarkersTask.Params.fullyReadEventId(): String? { + if (fullyReadEventId != null) { + return this.fullyReadEventId + } else { + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst() + val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() + return if (readMarker?.eventId == readReceipt?.eventId) { + readReceiptEventId + } else { + null + } + } + } + } + + } \ No newline at end of file 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 59d37a8062..0ded458a20 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 @@ -73,7 +73,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv }) } - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData { val liveData = RealmLiveData(monarchy.realmConfiguration) { TimelineEventEntity.where(it, eventId = eventId) } 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 45fbe7329d..99fbc5750d 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,8 +16,7 @@ package im.vector.matrix.android.internal.session.sync -import im.vector.matrix.android.api.session.room.read.FullyReadContent -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 344833cca7..906963d83a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent -import im.vector.matrix.android.api.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addStateEvent diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 3d4df602b7..2c518fa6ee 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -184,7 +184,7 @@ class PushrulesConditionTest { } class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -193,7 +193,7 @@ class PushrulesConditionTest { return _numberOfJoinedMembers } - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } 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 index f5f086ac8b..986acef616 100644 --- 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 @@ -23,8 +23,8 @@ import android.util.AttributeSet import android.view.View import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.core.view.isInvisible import im.vector.riotx.R -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import kotlinx.coroutines.* private const val DELAY_IN_MS = 1_500L @@ -36,30 +36,34 @@ class ReadMarkerView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { interface Callback { - fun onReadMarkerDisplayed() + fun onReadMarkerLongBound() } + private var eventId: String? = null private var callback: Callback? = null private var callbackDispatcherJob: Job? = null - fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) { + fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { + this.eventId = eventId this.callback = readMarkerCallback if (displayReadMarker) { - visibility = VISIBLE - callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { - delay(DELAY_IN_MS) - callback?.onReadMarkerDisplayed() - } startAnimation() } else { - visibility = INVISIBLE + this.animation?.cancel() + this.visibility = INVISIBLE + } + if (hasReadMarker) { + callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { + delay(DELAY_IN_MS) + callback?.onReadMarkerLongBound() + } } - } fun unbind() { this.callbackDispatcherJob?.cancel() this.callback = null + this.eventId = null this.animation?.cancel() this.visibility = INVISIBLE } @@ -80,6 +84,7 @@ class ReadMarkerView @JvmOverloads constructor( override fun onAnimationRepeat(animation: Animation) {} }) } + visibility = VISIBLE animation.start() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index 8c8bd1266f..5001449c3f 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -33,6 +33,10 @@ internal class Debouncer(private val handler: Handler) { return true } + fun cancelAll() { + handler.removeCallbacksAndMessages(null) + } + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { val chained = Runnable { handler.post(r) @@ -41,4 +45,6 @@ internal class Debouncer(private val handler: Handler) { runnables[identifier] = chained handler.postDelayed(chained, millis) } + + } \ No newline at end of file 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 48cdea6e59..0bb45ccfb8 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 @@ -44,7 +44,6 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach -import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -312,17 +311,21 @@ class RoomDetailFragment : } } + override fun onDestroy() { + debouncer.cancelAll() + super.onDestroy() + } + private fun setupJumpToBottomView() { - jumpToBottomView.isVisible = false + jumpToBottomView.visibility = View.INVISIBLE jumpToBottomView.setOnClickListener { + jumpToBottomView.visibility = View.INVISIBLE withState(roomDetailViewModel) { state -> - recyclerView.stopScroll() if (state.timeline?.isLive == false) { state.timeline.restartWithEventId(null) } else { layoutManager.scrollToPosition(0) } - jumpToBottomView.isVisible = false } } } @@ -398,17 +401,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -437,9 +440,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -490,33 +493,33 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } } private fun updateJumpToBottomViewVisibility() { - debouncer.debounce("jump_to_bottom_visibility", 100, Runnable { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { jumpToBottomView.show() @@ -684,7 +687,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - timelineEventController.setTimeline(state.timeline, state.highlightedEventId) + timelineEventController.update(state.timeline, state.highlightedEventId, state.hideReadMarker) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -747,7 +750,6 @@ class RoomDetailFragment : } } - private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent -> { @@ -945,15 +947,15 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } - override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) { + override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) - if (eventId != null) { - roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId)) + val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (nextReadMarkerId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) } } -// AutocompleteUserPresenter.Callback + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) 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 ee4b3c0423..afe4ea6681 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -40,13 +40,13 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership -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.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -62,6 +62,7 @@ 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.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser @@ -116,6 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeEventDisplayedActions() observeSummaryState() observeJumpToReadMarkerViewVisibility() + observeReadMarkerVisibility() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -156,7 +158,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -303,7 +305,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -312,12 +314,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + ?: "", messageContent?.type + ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -332,7 +334,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -635,29 +637,30 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun observeJumpToReadMarkerViewVisibility() { - Observable - .combineLatest( - room.rx().liveRoomSummary().map { + Observable.combineLatest( + room.rx().liveRoomSummary() + .map { val readMarkerId = it.readMarkerId if (readMarkerId == null) { Option.empty() } else { - val timelineEvent = room.getTimeLineEvent(readMarkerId) - Option.fromNullable(timelineEvent) - } - }.distinctUntilChanged(), - visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false), - Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible -> - if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) { - false - } else { - val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE } - val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex - readMarkerPosition < currentVisibleEventPosition + val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex + ?: Int.MIN_VALUE + Option.just(readMarkerIndex) } } - ) + .distinctUntilChanged(), + visibleEventsObservable.distinctUntilChanged(), + isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, + Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerIndex, currentVisibleEvent, isReadMarkerViewVisible -> + if (readMarkerIndex.isEmpty() || isReadMarkerViewVisible) { + false + } else { + val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex + readMarkerIndex.getOrElse { Int.MIN_VALUE } < currentVisibleEventPosition + } + } + ) .distinctUntilChanged() .subscribe { setState { copy(showJumpToReadMarker = it) } @@ -682,6 +685,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun observeReadMarkerVisibility() { + Observable + .combineLatest( + room.rx().liveReadMarker(), + room.rx().liveReadReceipt(), + BiFunction, Optional, Boolean> { readMarker, readReceipt -> + readMarker.getOrNull() == readReceipt.getOrNull() + } + ) + .throttleLast(250, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .startWith(false) + .subscribe { + setState { copy(hideReadMarker = it) } + } + .disposeOnClear() + } + + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 5e36cf42dc..bf11740fc0 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 @@ -53,7 +53,8 @@ data class RoomDetailViewState( val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, val showJumpToReadMarker: Boolean = false, - val highlightedEventId: String? = null + val highlightedEventId: String? = null, + val hideReadMarker: Boolean = false ) : 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/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt index f4cfe9eb5a..998428477b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -26,7 +26,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { + if (position == 0 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { layoutManager.scrollToPosition(0) } } 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 652f35fb67..b5a5fe8ca8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,8 +31,6 @@ 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.resources.UserPreferencesProvider -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -80,7 +78,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) - fun onReadMarkerLongDisplayed(informationData: MessageInformationData) + fun onReadMarkerLongDisplayed() } interface UrlClickCallback { @@ -142,7 +140,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec requestModelBuild() } - fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) { + fun update(timeline: Timeline?, eventIdToHighlight: String?, hideReadMarker: Boolean) { if (this.timeline != timeline) { this.timeline = timeline this.timeline?.listener = this @@ -155,22 +153,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } + var requestModelBuild = false if (this.eventIdToHighlight != eventIdToHighlight) { // Clear cache to force a refresh synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } } this.eventIdToHighlight = eventIdToHighlight - + requestModelBuild = true + } + if (this.hideReadMarker != hideReadMarker) { + this.hideReadMarker = hideReadMarker + requestModelBuild = true + } + if (requestModelBuild) { requestModelBuild() } } + private var hideReadMarker: Boolean = false private var eventIdToHighlight: String? = null override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { @@ -224,8 +230,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -251,7 +257,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, hideReadMarker, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } 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 dd50bbf190..a387f3f496 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 @@ -31,6 +31,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava fun create(event: TimelineEvent, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?, exception: Exception? = null): DefaultItem? { val text = if (exception == null) { @@ -39,7 +40,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava "an exception occurred when rendering the event ${event.root.eventId}" } - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, hideReadMarker) return DefaultItem_() .leftGuideline(avatarSizeProvider.leftGuideline) 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 92f586ab7b..663762850a 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 @@ -41,6 +41,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { event.root.eventId ?: return null @@ -64,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) + val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) 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 42f0688e50..80b3aa261b 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 @@ -51,18 +51,22 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { - var highlighted = false - var showReadMarker = false val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) if (prevSameTypeEvents.isEmpty()) { 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.displayReadMarker(sessionHolder.getActiveSession().myUserId)) { showReadMarker = true } @@ -81,7 +85,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) @@ -97,12 +101,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act mergeItemCollapseStates[event.localId] = it requestModelBuild() }, - showReadMarker = showReadMarker + readMarkerId = readMarkerId, + showReadMarker = isCollapsed && showReadMarker, + readReceiptsCallback = callback ) MergedHeaderItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlighted) + .highlighted(isCollapsed && highlighted) .attributes(attributes) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) 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 24d5abfd94..2cf5a60c44 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 @@ -76,11 +76,12 @@ class MessageItemFactory @Inject constructor( fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent) + val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker) if (event.root.isRedacted()) { //message is redacted @@ -97,7 +98,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, callback) + return noticeItemFactory.create(event, highlight, hideReadMarker, 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 bb301cdcbd..2f774cd9ec 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 @@ -36,9 +36,11 @@ class NoticeItemFactory @Inject constructor( fun create(event: TimelineEvent, highlight: Boolean, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { + val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, hideReadMarker) 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 d75d43f840..18254120af 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,13 +33,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me fun create(event: TimelineEvent, nextEvent: TimelineEvent?, eventIdToHighlight: String?, + hideReadMarker: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { when (event.root.getClearType()) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -51,22 +52,22 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, hideReadMarker, 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, callback) + messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, callback) + encryptedItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback) } } // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER -> defaultItemFactory.create(event, highlight, callback) + EventType.STICKER -> defaultItemFactory.create(event, highlight, hideReadMarker, callback) else -> { Timber.v("Type ${event.root.getClearType()} not handled") null @@ -74,7 +75,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } } catch (e: Exception) { Timber.e(e, "failed to create message item") - defaultItemFactory.create(event, highlight, callback, e) + defaultItemFactory.create(event, highlight, hideReadMarker, 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 b8a89a4669..453f7e4cd9 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 @@ -41,7 +41,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { + fun create(event: TimelineEvent, nextEvent: TimelineEvent?, hideReadMarker: Boolean): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -65,7 +65,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = event.displayReadMarker(session.myUserId) + val displayReadMarker = !hideReadMarker && event.displayReadMarker(session.myUserId) return MessageInformationData( eventId = eventId, @@ -91,6 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } .toList(), + hasReadMarker = event.hasReadMarker, displayReadMarker = displayReadMarker ) } 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 44a5e2bdfb..408a997efd 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 @@ -56,8 +56,9 @@ abstract class AbsMessageItem : BaseEventItem() { }) private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerDisplayed() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() } } @@ -106,7 +107,12 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) + holder.readMarkerView.bindView( + attributes.informationData.eventId, + attributes.informationData.hasReadMarker, + attributes.informationData.displayReadMarker, + _readMarkerCallback + ) if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false 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 f07575e1a5..de105b2261 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,9 @@ 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 @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedHeaderItem : BaseEventItem() { @@ -37,6 +39,13 @@ abstract class MergedHeaderItem : BaseEventItem() { attributes.mergeData.distinctBy { it.userId } } + private val _readMarkerCallback = object : ReadMarkerView.Callback { + + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() + } + } + override fun getViewType() = STUB_ID override fun bind(holder: Holder) { @@ -68,6 +77,16 @@ 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) } data class Data( @@ -78,10 +97,12 @@ 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, val onCollapsedStateChanged: (Boolean) -> Unit ) 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 041b6dbddd..09d51cacfd 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 @@ -34,6 +34,7 @@ data class MessageInformationData( val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, val readReceipts: List = emptyList(), + val hasReadMarker: Boolean = false, val displayReadMarker: Boolean = false ) : Parcelable 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 8e61a3be1f..89270ce026 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 @@ -38,8 +38,8 @@ abstract class NoticeItem : BaseEventItem() { }) private val _readMarkerCallback = object : ReadMarkerView.Callback { - override fun onReadMarkerDisplayed() { - attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + override fun onReadMarkerLongBound() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed() } } @@ -50,12 +50,17 @@ abstract class NoticeItem : BaseEventItem() { attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString() - ?: attributes.informationData.senderId, + ?: attributes.informationData.senderId, holder.avatarImageView ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback) + holder.readMarkerView.bindView( + attributes.informationData.eventId, + attributes.informationData.hasReadMarker, + attributes.informationData.displayReadMarker, + _readMarkerCallback + ) } override fun unbind(holder: Holder) {