diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 59db3b287c..1bc7514b3e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -241,6 +241,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val bobEventsListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop } override fun onTimelineUpdated(snapshot: List) { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index f720672e0b..7943a240e5 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -20,15 +20,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.internal.database.helper.add -import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.merge +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.SessionRealmModule import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent -import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject @@ -58,8 +58,11 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldAdd_whenNotAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) + + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) chunk.timelineEvents.size shouldEqual 1 } } @@ -68,65 +71,23 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) + val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) + chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap()) chunk.timelineEvents.size shouldEqual 1 } } - @Test - fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeRoomMemberEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 - } - } - - @Test - fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeMessageEvent() - chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 - } - } - - @Test - fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvents = createFakeListOfEvents(30) - val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size - chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS) - chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents - } - } - - @Test - fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() { - monarchy.runTransactionSync { realm -> - val chunk: ChunkEntity = realm.createObject() - val fakeEvents = createFakeListOfEvents(30) - val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size - val lastIsState = fakeEvents.last().isStateEvent() - val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents - chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS) - chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex - } - } - @Test fun merge_shouldAddEvents_whenMergingBackward() { monarchy.runTransactionSync { realm -> val chunk1: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.timelineEvents.size shouldEqual 60 } } @@ -140,9 +101,9 @@ internal class ChunkEntityTest : InstrumentedTest { val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10) chunk1.isLastForward = true chunk2.isLastForward = false - chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS) - chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS) + chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.timelineEvents.size shouldEqual 40 chunk1.isLastForward.shouldBeTrue() } @@ -155,9 +116,9 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } } @@ -169,19 +130,25 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } } private fun ChunkEntity.addAll(roomId: String, events: List, - direction: PaginationDirection, - stateIndexOffset: Int = 0) { + direction: PaginationDirection) { events.forEach { event -> - add(roomId, event, direction) + val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let { + realm.copyToRealmOrUpdate(it) + } + addTimelineEvent(roomId, fakeEvent, direction, emptyMap()) } } + + companion object { + private const val ROOM_ID = "roomId" + } } 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 0f9c2ae124..80376fb6ee 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 @@ -62,27 +62,7 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct } return eventsToMerge .forEach { - if (timelineEvents.find(it.eventId) == null) { - val eventId = it.eventId - if (timelineEvents.find(eventId) != null) { - return - } - val displayIndex = nextDisplayIndex(direction) - val localId = TimelineEventEntity.nextId(realm) - val copied = localRealm.createObject().apply { - this.localId = localId - this.root = it.root - this.eventId = it.eventId - this.roomId = it.roomId - this.annotations = it.annotations - this.readReceipts = it.readReceipts - this.displayIndex = displayIndex - this.senderAvatar = it.senderAvatar - this.senderName = it.senderName - this.isUniqueDisplayName = it.isUniqueDisplayName - } - timelineEvents.add(copied) - } + addTimelineEventFromMerge(localRealm, it, direction) } } @@ -108,7 +88,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberContentsByUser: HashMap) { + roomMemberContentsByUser: Map) { val eventId = eventEntity.eventId if (timelineEvents.find(eventId) != null) { return @@ -130,28 +110,60 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, val roomMemberContent = roomMemberContentsByUser[senderId] this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName - if (roomMemberContent?.displayName != null) { - val isHistoricalUnique = roomMemberContentsByUser.values.find { - it != roomMemberContent && it?.displayName == roomMemberContent.displayName - } == null - isUniqueDisplayName = if (isLastForward) { - val isLiveUnique = RoomMemberSummaryEntity - .where(realm, roomId) - .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, roomMemberContent.displayName) - .findAll().none { - !roomMemberContentsByUser.containsKey(it.userId) - } - isHistoricalUnique && isLiveUnique - } else { - isHistoricalUnique - } + isUniqueDisplayName = if (roomMemberContent?.displayName != null) { + computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser) } else { - isUniqueDisplayName = true + true } } timelineEvents.add(timelineEventEntity) } +private fun computeIsUnique( + realm: Realm, + roomId: String, + isLastForward: Boolean, + myRoomMemberContent: RoomMemberContent, + roomMemberContentsByUser: Map +): Boolean { + val isHistoricalUnique = roomMemberContentsByUser.values.find { + it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName + } == null + return if (isLastForward) { + val isLiveUnique = RoomMemberSummaryEntity + .where(realm, roomId) + .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName) + .findAll().none { + !roomMemberContentsByUser.containsKey(it.userId) + } + isHistoricalUnique && isLiveUnique + } else { + isHistoricalUnique + } +} + +private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) { + val eventId = timelineEventEntity.eventId + if (timelineEvents.find(eventId) != null) { + return + } + val displayIndex = nextDisplayIndex(direction) + val localId = TimelineEventEntity.nextId(realm) + val copied = realm.createObject().apply { + this.localId = localId + this.root = timelineEventEntity.root + this.eventId = timelineEventEntity.eventId + this.roomId = timelineEventEntity.roomId + this.annotations = timelineEventEntity.annotations + this.readReceipts = timelineEventEntity.readReceipts + this.displayIndex = displayIndex + this.senderAvatar = timelineEventEntity.senderAvatar + this.senderName = timelineEventEntity.senderName + this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName + } + timelineEvents.add(copied) +} + private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() ?: realm.createObject(eventEntity.eventId).apply { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 8bb01ba644..881a6a45b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -20,7 +20,6 @@ import androidx.annotation.WorkerThread import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.runBlocking import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean 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 1357170aaf..d3b0787b68 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 @@ -120,7 +120,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - handleUpdates(changeSet) + handleUpdates(results, changeSet) } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -185,7 +185,7 @@ internal class DefaultTimeline( .filterEventsWithSettings() .findAll() handleInitialLoad() - filteredEvents.addChangeListener(eventsChangeListener) + nonFilteredEvents.addChangeListener(eventsChangeListener) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() @@ -215,8 +215,8 @@ internal class DefaultTimeline( if (this::eventRelations.isInitialized) { eventRelations.removeAllChangeListeners() } - if (this::filteredEvents.isInitialized) { - filteredEvents.removeAllChangeListeners() + if (this::nonFilteredEvents.isInitialized) { + nonFilteredEvents.removeAllChangeListeners() } if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.dispose() @@ -452,7 +452,7 @@ internal class DefaultTimeline( var shouldFetchInitialEvent = false val currentInitialEventId = initialEventId val initialDisplayIndex = if (currentInitialEventId == null) { - filteredEvents.firstOrNull()?.displayIndex + nonFilteredEvents.firstOrNull()?.displayIndex } else { val initialEvent = nonFilteredEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) @@ -480,7 +480,7 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results */ - private fun handleUpdates(changeSet: OrderedCollectionChangeSet) { + private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { // If changeSet has deletion we are having a gap, so we clear everything if (changeSet.deletionRanges.isNotEmpty()) { clearAllValues() @@ -488,9 +488,9 @@ internal class DefaultTimeline( var postSnapshot = false changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(filteredEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) } else { - Pair(filteredEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) } val state = getState(direction) if (state.isPaginating) { @@ -503,7 +503,7 @@ internal class DefaultTimeline( } } changeSet.changes.forEach { index -> - val eventEntity = filteredEvents[index] + val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { buildTimelineEvent(eventEntity) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 6209fbcfdb..41920158c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -167,7 +167,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") - roomId.isBlank() if (direction == PaginationDirection.FORWARDS) { val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) if (currentChunk != currentLiveChunk) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index cfb8bff66a..7313fd4f79 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -395,4 +395,4 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } -} \ 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 e9e20eba8f..e09270b52b 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 @@ -350,9 +350,11 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE if (!roomDetailViewModel.timeline.isLive) { + scrollOnNewMessageCallback.forceScrollOnNextUpdate() roomDetailViewModel.timeline.restartWithEventId(null) + } else { + layoutManager.scrollToPosition(0) } - layoutManager.scrollToPosition(0) } jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( 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 6a1646f009..a18ee31e2c 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 @@ -27,12 +27,22 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val newTimelineEventIds = CopyOnWriteArrayList() + private var forceScroll = false fun addNewTimelineEventIds(eventIds: List) { newTimelineEventIds.addAll(0, eventIds) } + fun forceScrollOnNextUpdate() { + forceScroll = true + } + override fun onInserted(position: Int, count: Int) { + if (forceScroll) { + forceScroll = false + layoutManager.scrollToPosition(position) + return + } Timber.v("On inserted $count count at position: $position") if (layoutManager.findFirstVisibleItemPosition() != position) { return