diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index 06651f9ba3..008508ae19 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest { // val latch = CountDownLatch(2) // var timelineEvents: List = emptyList() // timeline.listener = object : Timeline.Listener { -// override fun onUpdated(snapshot: List) { +// override fun onTimelineUpdated(snapshot: List) { // if (snapshot.isNotEmpty()) { // if (initialLoad == 0) { // initialLoad = snapshot.size 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 85dbdcaa19..2280803e5c 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 @@ -65,7 +65,7 @@ interface Timeline { /** * This is the main method to enrich the timeline with new data. - * It will call the onUpdated method from [Listener] when the data will be processed. + * It will call the onTimelineUpdated method from [Listener] when the data will be processed. * It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row. */ fun paginate(direction: Direction, count: Int) @@ -106,7 +106,12 @@ interface Timeline { * Call when the timeline has been updated through pagination or sync. * @param snapshot the most up to date snapshot */ - fun onUpdated(snapshot: List) + fun onTimelineUpdated(snapshot: List) + + /** + * Called whenever an error we can't recover from occurred + */ + fun onTimelineFailure(throwable: Throwable) } /** 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 826b35254e..f05fa01444 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 @@ -21,12 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 126d13c5db..509beddbf0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.* import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev @@ -38,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import io.realm.Realm -import io.realm.kotlin.createObject import javax.inject.Inject internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, @@ -69,9 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null, updateMembers: Boolean = false) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) - + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { roomSummaryEntity.heroes.clear() 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 f42e50bdab..85bab5d706 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 @@ -638,7 +638,14 @@ internal class DefaultTimeline( private fun fetchEvent(eventId: String) { val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize) - cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) + cancelableBag += contextOfEventTask.configureWith(params) { + callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + postFailure(failure) + } + } + } + .executeBy(taskExecutor) } private fun postSnapshot() { @@ -651,7 +658,7 @@ internal class DefaultTimeline( val runnable = Runnable { synchronized(listeners) { listeners.forEach { - it.onUpdated(snapshot) + it.onTimelineUpdated(snapshot) } } } @@ -659,6 +666,20 @@ internal class DefaultTimeline( } } + private fun postFailure(throwable: Throwable) { + if (isReady.get().not()) { + return + } + val runnable = Runnable { + synchronized(listeners) { + listeners.forEach { + it.onTimelineFailure(throwable) + } + } + } + mainHandler.post(runnable) + } + private fun clearAllValues() { prevDisplayIndex = null nextDisplayIndex = null 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 0d9fb4e9e6..7030509bfc 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 @@ -18,6 +18,10 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.addOrUpdate +import im.vector.matrix.android.internal.database.helper.addStateEvent +import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create @@ -112,7 +116,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -125,7 +129,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null - || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null + || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) @@ -139,7 +143,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } else { nextChunk?.apply { this.prevToken = prevToken } } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { Timber.v("Reach end of $roomId") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 189c80d657..d99b9df4df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.R -import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index efcbdfff39..902dfd33a7 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -31,6 +31,7 @@ import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util.assertMainThread +import com.google.android.material.snackbar.Snackbar import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.ScreenComponent @@ -167,6 +168,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { return this } + protected fun showErrorInSnackbar(throwable: Throwable) { + vectorBaseActivity.coordinatorLayout?.let { + Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) + .show() + } + } + /* ========================================================================================== * Toolbar * ========================================================================================== */ 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 8887b94f92..64544a9035 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 @@ -274,6 +274,16 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.requestLiveData.observeEvent(this) { displayRoomDetailActionResult(it) } + + roomDetailViewModel.viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + when (it) { + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + } + } + .disposeOnDestroyView() } override fun onActivityCreated(savedInstanceState: Bundle?) { 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 new file mode 100644 index 0000000000..a1ad480584 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +/** + * Transient events for RoomDetail + */ +sealed class RoomDetailViewEvents { + data class Failure(val throwable: Throwable) : 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 614a75265d..467148302f 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 @@ -27,6 +27,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage @@ -56,7 +57,9 @@ import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel 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.PublishDataSource import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand @@ -101,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timelineEvents = PublishRelay.create>() private var timeline = room.createTimeline(eventId, timelineSettings) + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents + // Can be used for several actions, for a one shot result private val _requestLiveData = MutableLiveData>>() val requestLiveData: LiveData>> @@ -862,10 +868,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - override fun onUpdated(snapshot: List) { + override fun onTimelineUpdated(snapshot: List) { timelineEvents.accept(snapshot) } + override fun onTimelineFailure(throwable: Throwable) { + // If we have a critical timeline issue, we get back to live. + timeline.restartWithEventId(null) + _viewEvents.post(RoomDetailViewEvents.Failure(throwable)) + } + override fun onCleared() { timeline.dispose() timeline.removeListener(this) 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 0f187fa9e5..582544ce8a 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 @@ -236,10 +236,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Timeline.LISTENER *************************************************************************** - override fun onUpdated(snapshot: List) { + override fun onTimelineUpdated(snapshot: List) { submitSnapshot(snapshot) } + override fun onTimelineFailure(throwable: Throwable) { + // no-op, already handled + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 9e54d5fc79..27fbc387b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -104,7 +104,7 @@ class RoomListFragment @Inject constructor( .subscribe { when (it) { is RoomListViewEvents.SelectRoom -> openSelectedRoom(it) - is RoomListViewEvents.Failure -> showError(it) + is RoomListViewEvents.Failure -> showErrorInSnackbar(it.throwable) } } .disposeOnDestroyView() @@ -135,13 +135,6 @@ class RoomListFragment @Inject constructor( } } - private fun showError(event: RoomListViewEvents.Failure) { - vectorBaseActivity.coordinatorLayout?.let { - Snackbar.make(it, errorFormatter.toHumanReadable(event.throwable), Snackbar.LENGTH_SHORT) - .show() - } - } - private fun setupCreateRoomButton() { when (roomListParams.displayMode) { RoomListDisplayMode.HOME -> createChatFabMenu.isVisible = true