From fee2ec6b6690a98e5b7cc32959fce806fad3061f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 20 Jan 2020 21:13:53 +0100 Subject: [PATCH] Scroll when event build come from sync/send + remove use of monarchy writeAsync --- .../api/session/room/send/DraftService.kt | 6 +- .../api/session/room/timeline/Timeline.kt | 5 + .../database/helper/RoomEntityHelper.kt | 5 +- .../database/mapper/TimelineEventMapper.kt | 9 +- .../session/group/DefaultGetGroupDataTask.kt | 5 +- .../internal/session/room/RoomFactory.kt | 30 ++-- .../session/room/draft/DefaultDraftService.kt | 134 ++------------- .../session/room/draft/DraftRepository.kt | 156 ++++++++++++++++++ .../room/relation/DefaultRelationService.kt | 2 +- .../session/room/send/DefaultSendService.kt | 103 ++++-------- .../room/send/LocalEchoEventFactory.kt | 44 +++-- .../session/room/send/LocalEchoRepository.kt | 147 +++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 31 +++- .../room/timeline/DefaultTimelineService.kt | 26 +-- .../internal/session/sync/RoomSyncHandler.kt | 15 +- .../android/internal/task/TaskExecutor.kt | 2 +- .../riotx/core/utils/NoOpMatrixCallback.kt | 21 +++ .../home/room/detail/RoomDetailFragment.kt | 1 + .../home/room/detail/RoomDetailViewEvents.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 45 +++-- .../room/detail/ScrollOnNewMessageCallback.kt | 18 +- .../timeline/TimelineEventController.kt | 7 +- 22 files changed, 538 insertions(+), 275 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt index 2324f1e221..ffb15f4632 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt @@ -17,18 +17,20 @@ package im.vector.matrix.android.api.session.room.send import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable interface DraftService { /** * Save or update a draft to the room */ - fun saveDraft(draft: UserDraft) + fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable /** * Delete the last draft, basically just after sending the message */ - fun deleteDraft() + fun deleteDraft(callback: MatrixCallback): Cancelable /** * Return the current drafts if any, as a live data 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 2280803e5c..16bf522c59 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 @@ -112,6 +112,11 @@ interface Timeline { * Called whenever an error we can't recover from occurred */ fun onTimelineFailure(throwable: Throwable) + + /** + * Call when new events come through the sync + */ + fun onNewTimelineEvents(eventIds: List) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index b6c6c6c1ac..9d48d477bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import io.realm.Realm internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { chunks.remove(chunkEntity) @@ -53,8 +54,7 @@ internal fun RoomEntity.addStateEvent(stateEvent: Event, untimelinedStateEvents.add(entity) } } -internal fun RoomEntity.addSendingEvent(event: Event) { - assertIsManaged() +internal fun RoomEntity.addSendingEvent(realm: Realm, event: Event) { val senderId = event.senderId ?: return val eventEntity = event.toEntity(roomId).apply { this.sendState = SendState.UNSENT @@ -72,3 +72,4 @@ internal fun RoomEntity.addSendingEvent(event: Event) { } sendingTimelineEvents.add(0, timelineEventEntity) } + 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 9959f940b6..146b5c3ae4 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 @@ -43,9 +43,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = timelineEventEntity.senderAvatar, - readReceipts = readReceipts?.sortedByDescending { - it.originServerTs - } ?: emptyList() + readReceipts = readReceipts + ?.distinctBy { + it.user + }?.sortedByDescending { + it.originServerTs + } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 069c9b8d21..a24eecd251 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import org.greenrobot.eventbus.EventBus import javax.inject.Inject @@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor( insertInDb(groupSummary, groupRooms, groupUsers, groupId) } - private fun insertInDb(groupSummary: GroupSummaryResponse, + private suspend fun insertInDb(groupSummary: GroupSummaryResponse, groupRooms: GroupRooms, groupUsers: GroupUsers, groupId: String) { monarchy - .writeAsync { realm -> + .awaitTransaction { realm -> val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() ?: realm.createObject(GroupSummaryEntity::class.java, groupId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index b24bb73d56..cda43718dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -50,25 +50,25 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val typingServiceFactory: DefaultTypingService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, - private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) : + private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory): RoomFactory { override fun create(roomId: String): Room { return DefaultRoom( - roomId, - monarchy, - roomSummaryMapper, - timelineServiceFactory.create(roomId), - sendServiceFactory.create(roomId), - draftServiceFactory.create(roomId), - stateServiceFactory.create(roomId), - reportingServiceFactory.create(roomId), - readServiceFactory.create(roomId), - typingServiceFactory.create(roomId), - cryptoService, - relationServiceFactory.create(roomId), - membershipServiceFactory.create(roomId), - roomPushRuleServiceFactory.create(roomId) + roomId = roomId, + monarchy = monarchy, + roomSummaryMapper = roomSummaryMapper, + timelineService = timelineServiceFactory.create(roomId), + sendService = sendServiceFactory.create(roomId), + draftService = draftServiceFactory.create(roomId), + stateService = stateServiceFactory.create(roomId), + reportingService = reportingServiceFactory.create(roomId), + readService = readServiceFactory.create(roomId), + typingService = typingServiceFactory.create(roomId), + cryptoService = cryptoService, + relationService = relationServiceFactory.create(roomId), + roomMembersService = membershipServiceFactory.create(roomId), + roomPushRuleService = roomPushRuleServiceFactory.create(roomId) ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt index 9fd9cf7c9d..99ec5ede4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt @@ -17,23 +17,21 @@ package im.vector.matrix.android.internal.session.room.draft import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.UserDraft -import im.vector.matrix.android.internal.database.mapper.DraftMapper -import im.vector.matrix.android.internal.database.model.DraftEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.UserDraftsEntity -import im.vector.matrix.android.internal.database.query.where -import io.realm.kotlin.createObject -import timber.log.Timber +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, - private val monarchy: Monarchy + private val draftRepository: DraftRepository, + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : DraftService { @AssistedInject.Factory @@ -45,121 +43,19 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, * or even move an existing draft to the top of the list */ - override fun saveDraft(draft: UserDraft) { - Timber.d("Draft: saveDraft ${privacySafe(draft)}") - - monarchy.writeAsync { realm -> - - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - - val userDraftsEntity = roomSummaryEntity.userDrafts - ?: realm.createObject().also { - roomSummaryEntity.userDrafts = it - } - - userDraftsEntity.let { userDraftEntity -> - // Save only valid draft - if (draft.isValid()) { - // Add a new draft or update the current one? - val newDraft = DraftMapper.map(draft) - - // Is it an update of the top draft? - val topDraft = userDraftEntity.userDrafts.lastOrNull() - - if (topDraft == null) { - Timber.d("Draft: create a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { - // top draft is an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - if (topDraft.linkedEventId == newDraft.linkedEventId) { - // Update the top draft - Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") - topDraft.content = newDraft.content - } else { - // Check a previously EDIT draft with the same id - val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { - it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId - } - - if (existingEditDraftOfSameEvent != null) { - // Ignore the new text, restore what was typed before, by putting the draft to the top - Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) - userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) - } else { - Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } - } else { - // Add a new regular draft to the top - Timber.d("Draft: add a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } else { - // Top draft is not an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else { - // Update the top draft - Timber.d("Draft: update the top draft ${privacySafe(draft)}") - topDraft.draftMode = newDraft.draftMode - topDraft.content = newDraft.content - topDraft.linkedEventId = newDraft.linkedEventId - } - } - } else { - // There is no draft to save, so the composer was clear - Timber.d("Draft: delete a draft") - - val topDraft = userDraftEntity.userDrafts.lastOrNull() - - if (topDraft == null) { - Timber.d("Draft: nothing to do") - } else { - // Remove the top draft - Timber.d("Draft: remove the top draft") - userDraftEntity.userDrafts.remove(topDraft) - } - } - } + override fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.saveDraft(roomId, draft) } } - private fun privacySafe(o: Any): Any { - if (BuildConfig.LOG_PRIVATE_DATA) { - return o - } - - return "" - } - - override fun deleteDraft() { - Timber.d("Draft: deleteDraft()") - - monarchy.writeAsync { realm -> - UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> - if (userDraftsEntity.userDrafts.isNotEmpty()) { - userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) - } - } + override fun deleteDraft(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + draftRepository.deleteDraft(roomId) } } override fun getDraftsLive(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { UserDraftsEntity.where(it, roomId) }, - { - it.userDrafts.map { draft -> - DraftMapper.map(draft) - } - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: emptyList() - } + return draftRepository.getDraftsLive(roomId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt new file mode 100644 index 0000000000..b00bf2aadb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DraftRepository.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.internal.database.mapper.DraftMapper +import im.vector.matrix.android.internal.database.model.DraftEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.Realm +import io.realm.kotlin.createObject +import timber.log.Timber +import javax.inject.Inject + +class DraftRepository @Inject constructor(private val monarchy: Monarchy) { + + suspend fun saveDraft(roomId: String, userDraft: UserDraft) { + monarchy.awaitTransaction { + saveDraft(it, userDraft, roomId) + } + } + + suspend fun deleteDraft(roomId: String) { + monarchy.awaitTransaction { + deleteDraft(it, roomId) + } + } + + private fun deleteDraft(realm: Realm, roomId: String) { + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + + private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + + fun getDraftsLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: emptyList() + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + return "" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 1b2b27a3eb..867c9eab1f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -242,6 +242,6 @@ internal class DefaultRelationService @AssistedInject constructor( * the same transaction id is received (in unsigned data) */ private fun saveLocalEcho(event: Event) { - eventFactory.saveLocalEcho(monarchy, event) + eventFactory.createLocalEcho(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 507c8dd247..5061b5f2c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -17,35 +17,35 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import androidx.work.WorkManager import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.* -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.events.model.Event +import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.room.send.SendService 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.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag -import im.vector.matrix.android.internal.database.mapper.asDomain -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.where import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.startChain +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -59,7 +59,9 @@ internal class DefaultSendService @AssistedInject constructor( @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository ) : SendService { @AssistedInject.Factory @@ -71,15 +73,14 @@ internal class DefaultSendService @AssistedInject constructor( override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { - saveLocalEcho(it) + createLocalEcho(it) } - return sendEvent(event) } override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { - saveLocalEcho(it) + createLocalEcho(it) } return sendEvent(event) @@ -157,13 +158,8 @@ internal class DefaultSendService @AssistedInject constructor( } override fun deleteFailedEcho(localEcho: TimelineEvent) { - monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, localEcho) } } @@ -181,67 +177,26 @@ internal class DefaultSendService @AssistedInject constructor( .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) .enqueue() } - - monarchy.writeAsync { realm -> - RoomEntity.where(realm, roomId).findFirst()?.let { room -> - room.sendingTimelineEvents.forEach { - it.root?.sendState = SendState.UNDELIVERED - } - } + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) } } override fun resendAllFailedMessages() { - monarchy.writeAsync { realm -> - TimelineEventEntity - .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) - .sortedBy { it.root?.originServerTs ?: 0 } - .forEach { timelineEventEntity -> - timelineEventEntity.root?.let { - val event = it.asDomain() - when (event.getClearType()) { - EventType.MESSAGE, - EventType.REDACTION, - EventType.REACTION -> { - val content = event.getClearContent().toModel() - if (content != null) { - when (content.type) { - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_LOCATION, - MessageType.MSGTYPE_TEXT -> { - it.sendState = SendState.UNSENT - sendEvent(event) - } - MessageType.MSGTYPE_FILE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_AUDIO -> { - // need to resend the attachement - } - else -> { - Timber.e("Cannot resend message ${event.type} / ${content.type}") - } - } - } else { - Timber.e("Unsupported message to resend ${event.type}") - } - } - else -> { - Timber.e("Unsupported message to resend ${event.type}") - } - } - } - } + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) + } + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) } } override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - saveLocalEcho(it) + createLocalEcho(it) } - return internalSendMedia(event, attachment) } @@ -276,8 +231,8 @@ internal class DefaultSendService @AssistedInject constructor( return CancelableWork(context, sendWork.id) } - private fun saveLocalEcho(event: Event) { - localEchoEventFactory.saveLocalEcho(monarchy, event) + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) } private fun buildWorkName(identifier: String): String { @@ -306,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor( private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { - saveLocalEcho(it) + createLocalEcho(it) } val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) 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 7a935783cf..383c961aaf 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 @@ -18,26 +18,42 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever import androidx.exifinterface.media.ExifInterface -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.LocalEcho +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.AudioInfo +import im.vector.matrix.android.api.session.room.model.message.FileInfo +import im.vector.matrix.android.api.session.room.model.message.ImageInfo +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo +import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent -import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.content.ThumbnailExtractor -import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider +import kotlinx.coroutines.launch import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import javax.inject.Inject @@ -54,8 +70,9 @@ import javax.inject.Inject internal class LocalEchoEventFactory @Inject constructor( @UserId private val userId: String, private val stringProvider: StringProvider, - private val roomSummaryUpdater: RoomSummaryUpdater, - private val textPillsUtils: TextPillsUtils + private val textPillsUtils: TextPillsUtils, + private val taskExecutor: TaskExecutor, + private val localEchoRepository: LocalEchoRepository ) { // TODO Inject private val parser = Parser.builder().build() @@ -402,13 +419,10 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun saveLocalEcho(monarchy: Monarchy, event: Event) { + fun createLocalEcho(event: Event){ checkNotNull(event.roomId) { "Your event should have a roomId" } - monarchy.writeAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst() - ?: return@writeAsync - roomEntity.addSendingEvent(event) - roomSummaryUpdater.update(realm, event.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 new file mode 100644 index 0000000000..5cdf4d1d4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.send + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Event +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.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.nextId +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.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.where +import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline +import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, + private val roomSummaryUpdater: RoomSummaryUpdater, + private val eventBus: EventBus) { + + 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 eventEntity = event.toEntity(roomId).apply { + this.sendState = SendState.UNSENT + } + val roomMemberHelper = RoomMemberHelper(realm, roomId) + val myUser = roomMemberHelper.getLastRoomMember(senderId) + val localId = TimelineEventEntity.nextId(realm) + val timelineEventEntity = TimelineEventEntity(localId).also { + it.root = eventEntity + it.eventId = event.eventId + it.roomId = roomId + it.senderName = myUser?.displayName + it.senderAvatar = myUser?.avatarUrl + it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) + } + roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) + roomSummaryUpdater.update(realm, roomId) + } + } + + suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + monarchy.awaitTransaction { realm -> + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { + it.deleteFromRealm() + } + } + } + + suspend fun clearSendingQueue(roomId: String) { + monarchy.awaitTransaction { realm -> + RoomEntity.where(realm, roomId).findFirst()?.let { room -> + room.sendingTimelineEvents.forEach { + it.root?.sendState = SendState.UNDELIVERED + } + } + } + } + + suspend fun updateSendState(roomId: String, eventIds: List, sendState: SendState) { + monarchy.awaitTransaction { realm -> + val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() + timelineEvents.forEach { + it.root?.sendState = sendState + } + } + } + + fun getAllFailedEventsToResend(roomId: String): List { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .sortedByDescending { it.root?.displayIndex ?: 0 } + .mapNotNull { it.root?.asDomain() } + .filter { event -> + when (event.getClearType()) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION -> { + val content = event.getClearContent().toModel() + if (content != null) { + when (content.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_TEXT -> { + true + } + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO -> { + // need to resend the attachement + false + } + else -> { + Timber.e("Cannot resend message ${event.type} / ${content.type}") + false + } + } + } else { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + else -> { + Timber.e("Unsupported message to resend ${event.type}") + false + } + } + } + } + } +} 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 573a46f10a..62268349b3 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 @@ -50,6 +50,9 @@ import io.realm.RealmConfiguration import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import java.util.Collections import java.util.UUID @@ -72,9 +75,12 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val eventBus: EventBus ) : Timeline, TimelineHiddenReadReceipts.Delegate { + data class OnNewTimelineEvents(val roomId: String, val eventIds: List) + companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } @@ -128,7 +134,7 @@ internal class DefaultTimeline( if (hasChange) postSnapshot() } -// Public methods ****************************************************************************** + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { BACKGROUND_HANDLER.post { @@ -159,6 +165,7 @@ internal class DefaultTimeline( override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + eventBus.register(this) BACKGROUND_HANDLER.post { eventDecryptor.start() val realm = Realm.getInstance(realmConfiguration) @@ -190,12 +197,13 @@ internal class DefaultTimeline( } private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { - return settings.buildReadReceipts && (settings.filterEdits || settings.filterTypes) + return buildReadReceipts && (filterEdits || filterTypes) } override fun dispose() { if (isStarted.compareAndSet(true, false)) { isReady.set(false) + eventBus.unregister(this) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") cancelableBag.cancel() BACKGROUND_HANDLER.removeCallbacksAndMessages(null) @@ -316,6 +324,15 @@ internal class DefaultTimeline( postSnapshot() } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { + if (onNewTimelineEvents.roomId == roomId) { + listeners.forEach { + it.onNewTimelineEvents(onNewTimelineEvents.eventIds) + } + } + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -401,14 +418,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -506,10 +523,10 @@ internal class DefaultTimeline( this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> 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 d92dbd66be..5ed3c76ed6 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 @@ -34,9 +34,11 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap +import org.greenrobot.eventbus.EventBus internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, + private val eventBus: EventBus, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, private val cryptoService: CryptoService, @@ -52,17 +54,19 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv } override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { - return DefaultTimeline(roomId, - eventId, - monarchy.realmConfiguration, - taskExecutor, - contextOfEventTask, - clearUnlinkedEventsTask, - paginationTask, - cryptoService, - timelineEventMapper, - settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) + return DefaultTimeline( + roomId = roomId, + initialEventId = eventId, + realmConfiguration = monarchy.realmConfiguration, + taskExecutor = taskExecutor, + contextOfEventTask = contextOfEventTask, + clearUnlinkedEventsTask = clearUnlinkedEventsTask, + paginationTask = paginationTask, + cryptoService = cryptoService, + timelineEventMapper = timelineEventMapper, + settings = settings, + hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + eventBus = eventBus ) } 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 a1699d9f55..9a24eb502a 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 @@ -36,11 +36,13 @@ import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.typing.TypingEventContent import im.vector.matrix.android.internal.session.sync.model.* import io.realm.Realm import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -50,7 +52,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomFullyReadHandler: RoomFullyReadHandler, private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, - private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { + private val timelineEventSenderVisitor: TimelineEventSenderVisitor, + private val eventBus: EventBus) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -130,6 +133,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle if (roomSync.timeline?.events?.isNotEmpty() == true) { val chunkEntity = handleTimelineEvents( realm, + roomId, roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, @@ -161,7 +165,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { - val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) + val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { @@ -183,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } private fun handleTimelineEvents(realm: Realm, + roomId: String, roomEntity: RoomEntity, eventList: List, prevToken: String? = null, @@ -202,7 +207,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle chunkEntity.isUnlinked = false val timelineEvents = ArrayList(eventList.size) + val eventIds = ArrayList(eventList.size) for (event in eventList) { + if(event.eventId != null) { + eventIds.add(event.eventId) + } chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also { timelineEvents.add(it) } @@ -221,6 +230,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomMemberEventHandler.handle(realm, roomEntity.roomId, event) } timelineEventSenderVisitor.visit(timelineEvents) + // posting new events to timeline if any is registered + eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = eventIds)) return chunkEntity } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index 8dcf85f707..8dcc15dec6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -35,7 +35,7 @@ import kotlin.coroutines.EmptyCoroutineContext @MatrixScope internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { - private val executorScope = CoroutineScope(SupervisorJob()) + val executorScope = CoroutineScope(SupervisorJob()) fun execute(task: ConfigurableTask): Cancelable { return executorScope diff --git a/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt b/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt new file mode 100644 index 0000000000..f24fc2a09c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/NoOpMatrixCallback.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import im.vector.matrix.android.api.MatrixCallback + +class NoOpMatrixCallback: MatrixCallback 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 27983aa487..45d1ec1c71 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 @@ -312,6 +312,7 @@ class RoomDetailFragment @Inject constructor( .subscribe { when (it) { is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) } } .disposeOnDestroyView() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index a1ad480584..4de399c838 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -21,4 +21,5 @@ package im.vector.riotx.features.home.room.detail */ sealed class RoomDetailViewEvents { data class Failure(val throwable: Throwable) : RoomDetailViewEvents() + data class OnNewTimelineEvents(val eventIds: List) : RoomDetailViewEvents() } 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 7d48124c5a..0a375ac328 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 @@ -20,7 +20,12 @@ import android.net.Uri import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import com.squareup.inject.assisted.Assisted @@ -58,6 +63,7 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.DataSource import im.vector.riotx.core.utils.LiveEvent +import im.vector.riotx.core.utils.NoOpMatrixCallback import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser @@ -218,10 +224,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { withState { when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) } } } @@ -465,7 +471,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun popDraft() { - room.deleteDraft() + room.deleteDraft(object : MatrixCallback {}) } private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { @@ -584,7 +590,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } timelineEvent.root.eventId?.let { - room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) + room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""), NoOpMatrixCallback()) } } } @@ -598,9 +604,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.QUOTE(it, "")) + room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback()) } else { - room.saveDraft(UserDraft.QUOTE(it, action.text)) + room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback()) } } } @@ -616,9 +622,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.REPLY(it, "")) + room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback()) } else { - room.saveDraft(UserDraft.REPLY(it, action.text)) + room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback()) } } } @@ -630,10 +636,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro withState { if (draft.isNotBlank()) { when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft)) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft)) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft)) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback()) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) } } } @@ -644,10 +650,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro withState { state -> // For edit, just delete the current draft if (state.sendMode is SendMode.EDIT) { - room.deleteDraft() + room.deleteDraft(NoOpMatrixCallback()) } else { // Save a new draft and keep the previously entered text - room.saveDraft(UserDraft.REGULAR(action.text)) + room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback()) } } } @@ -892,6 +898,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) } + override fun onNewTimelineEvents(eventIds: List) { + Timber.v("On new timeline events: $eventIds") + _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) + } + override fun onCleared() { timeline.dispose() timeline.removeAllListeners() 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 2e2945c59e..2180571c3b 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 @@ -19,15 +19,29 @@ package im.vector.riotx.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager 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 class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { + private val newTimelineEventIds = HashSet() + + fun addNewTimelineEventIds(eventIds: List){ + newTimelineEventIds.addAll(eventIds) + } + override fun onInserted(position: Int, count: Int) { Timber.v("On inserted $count count at position: $position") - if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) { - layoutManager.scrollToPosition(0) + if(layoutManager.findFirstVisibleItemPosition() != position ){ + return + } + val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return + val firstNewItemIds = firstNewItem.getEventIds() + if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){ + Timber.v("Should scroll to position: $position") + newTimelineEventIds.clear() + 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 a08669da3b..1579a77779 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,6 +30,7 @@ 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 @@ -241,6 +242,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // no-op, already handled } + override fun onNewTimelineEvents(eventIds: List) { + // no-op, already handled + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true @@ -346,8 +351,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return positionOfReadMarker } - fun isLoadingForward() = showingForwardLoader - private data class CacheItemData( val localId: Long, val eventId: String?,