diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index b16c865765..f348a8b75c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -106,6 +106,10 @@ class CommonTestHelper(context: Context) { override fun onTimelineFailure(throwable: Throwable) { } + override fun onNewTimelineEvents(eventIds: List) { + //noop + } + override fun onTimelineUpdated(snapshot: List) { // TODO Count only new messages? if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) { 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 16bf522c59..164afb3a60 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 @@ -114,7 +114,7 @@ interface Timeline { fun onTimelineFailure(throwable: Throwable) /** - * Call when new events come through the sync + * Called when new events come through the sync */ fun onNewTimelineEvents(eventIds: 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 caa64a85f8..0c8a04db36 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 @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten data class TimelineEvent( val root: Event, val localId: Long, + val eventId: String, val displayIndex: Int, val senderName: String?, val isUniqueDisplayName: Boolean, 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 aa282fb673..4bd9b9855b 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 @@ -37,6 +37,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS return TimelineEvent( root = timelineEventEntity.root?.asDomain() ?: Event("", timelineEventEntity.eventId), + eventId = timelineEventEntity.eventId, annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.displayIndex, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 383c961aaf..0db7377d4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.util.StringProvider import kotlinx.coroutines.launch import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +import timber.log.Timber import javax.inject.Inject /** @@ -419,7 +420,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun createLocalEcho(event: Event){ + fun createLocalEcho(event: Event) { checkNotNull(event.roomId) { "Your event should have a roomId" } taskExecutor.executorScope.launch { localEchoRepository.createLocalEcho(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index a78ae3cdec..bc0f413e92 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -24,13 +24,17 @@ 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.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.nextId +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper @@ -39,24 +43,27 @@ import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import org.greenrobot.eventbus.EventBus import timber.log.Timber +import java.lang.IllegalStateException import javax.inject.Inject +import kotlin.random.Random internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, private val roomSummaryUpdater: RoomSummaryUpdater, - private val eventBus: EventBus) { + private val eventBus: EventBus, + private val timelineEventMapper: TimelineEventMapper) { suspend fun createLocalEcho(event: Event) { - val roomId = event.roomId ?: return - val senderId = event.senderId ?: return - val eventId = event.eventId ?: return - eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId))) - monarchy.awaitTransaction { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction + val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") + val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event") + if (event.eventId == null) { + throw IllegalStateException("You should have set an eventId for your event") + } + val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val eventEntity = event.toEntity(roomId, SendState.UNSENT) val roomMemberHelper = RoomMemberHelper(realm, roomId) val myUser = roomMemberHelper.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) - val timelineEventEntity = TimelineEventEntity(localId).also { + TimelineEventEntity(localId).also { it.root = eventEntity it.eventId = event.eventId it.roomId = roomId @@ -64,6 +71,11 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon it.senderAvatar = myUser?.avatarUrl it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) } + } + val timelineEvent = timelineEventMapper.map(timelineEventEntity) + eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) } 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 de71378ba1..b69b25d09a 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 @@ -78,6 +78,7 @@ internal class DefaultTimeline( ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) + data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent) companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -99,6 +100,7 @@ internal class DefaultTimeline( private var prevDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null + private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsState = AtomicReference(State()) @@ -321,13 +323,24 @@ internal class DefaultTimeline( @Subscribe(threadMode = ThreadMode.MAIN) fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { - if (onNewTimelineEvents.roomId == roomId) { + if (isLive && onNewTimelineEvents.roomId == roomId) { listeners.forEach { it.onNewTimelineEvents(onNewTimelineEvents.eventIds) } } } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { + if (isLive && onLocalEchoCreated.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot() + } + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -394,12 +407,15 @@ internal class DefaultTimeline( private fun buildSendingEvents(): List { val sendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { + sendingEvents.addAll(inMemorySendingEvents) roomEntity?.sendingTimelineEvents ?.where() ?.filterEventsWithSettings() ?.findAll() - ?.forEach { - sendingEvents.add(timelineEventMapper.map(it)) + ?.forEach { timelineEventEntity -> + if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { + sendingEvents.add(timelineEventMapper.map(timelineEventEntity)) + } } } return sendingEvents @@ -580,6 +596,11 @@ internal class DefaultTimeline( offsetResults.forEach { eventEntity -> val timelineEvent = buildTimelineEvent(eventEntity) + val transactionId = timelineEvent.root.unsignedData?.transactionId + val sendingEvent = inMemorySendingEvents.find { + it.eventId == transactionId + } + inMemorySendingEvents.remove(sendingEvent) if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { @@ -665,7 +686,7 @@ internal class DefaultTimeline( it.onTimelineUpdated(snapshot) } } - debouncer.debounce("post_snapshot", runnable, 50) + debouncer.debounce("post_snapshot", runnable, 1) } } 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 2180571c3b..6a1646f009 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 @@ -21,26 +21,30 @@ import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem import timber.log.Timber +import java.util.concurrent.CopyOnWriteArrayList class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { - private val newTimelineEventIds = HashSet() + private val newTimelineEventIds = CopyOnWriteArrayList() - fun addNewTimelineEventIds(eventIds: List){ - newTimelineEventIds.addAll(eventIds) + fun addNewTimelineEventIds(eventIds: List) { + newTimelineEventIds.addAll(0, eventIds) } override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if(layoutManager.findFirstVisibleItemPosition() != position ){ + if (layoutManager.findFirstVisibleItemPosition() != position) { return } val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return - val firstNewItemIds = firstNewItem.getEventIds() - if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){ + val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() + val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) + if (indexOfFirstNewItem != -1) { Timber.v("Should scroll to position: $position") - newTimelineEventIds.clear() + repeat(newTimelineEventIds.size - indexOfFirstNewItem) { + newTimelineEventIds.removeAt(indexOfFirstNewItem) + } layoutManager.scrollToPosition(position) } } 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 1579a77779..cdc75477fb 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 @@ -30,7 +30,6 @@ 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.epoxy.emptyItem import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.UnreadState @@ -253,7 +252,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec currentSnapshot = newSnapshot val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) - requestDelayedModelBuild(100) + requestModelBuild() inSubmitList = false } }