diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 7c75f36a3b..d9f94ba27b 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -794,6 +794,7 @@ Shows all threads you’ve participated in Keep discussions organized with threads Threads help keep your conversations on-topic and easy to track. + You\'re homeserver does not support listing threads yet. Tip: Long tap a message and use “%s”. From a Thread diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 5b41ddaaec..165dcf079e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -25,6 +25,9 @@ import java.io.IOException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection +fun Throwable.is400() = this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_BAD_REQUEST + fun Throwable.is401() = this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ error.code == MatrixError.M_UNAUTHORIZED diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt index 5d4d67a65e..e3c5deeee7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -19,5 +19,4 @@ package org.matrix.android.sdk.api.session.room.threads sealed class FetchThreadsResult { data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() object ReachedEnd : FetchThreadsResult() - object Failed : FetchThreadsResult() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt new file mode 100644 index 0000000000..7dda460a5e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewActions.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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.app.features.home.room.threads.list.viewmodel + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface ThreadListViewActions : VectorViewModelAction { + object TryAgain : ThreadListViewActions +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt new file mode 100644 index 0000000000..3e9af034f4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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.app.features.home.room.threads.list.viewmodel + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface ThreadListViewEvents : VectorViewEvents { + data class ShowError(val throwable: Throwable) : ThreadListViewEvents +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 7124727bb7..f31f19849c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -27,8 +27,6 @@ import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsInteraction @@ -52,7 +50,7 @@ class ThreadListViewModel @AssistedInject constructor( @Assisted val initialState: ThreadListViewState, private val analyticsTracker: AnalyticsTracker, private val session: Session, -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) @@ -93,7 +91,17 @@ class ThreadListViewModel @AssistedInject constructor( fetchAndObserveThreads() } - override fun handle(action: EmptyAction) {} + override fun handle(action: ThreadListViewActions) { + when (action) { + ThreadListViewActions.TryAgain -> handleTryAgain() + } + } + + private fun handleTryAgain() { + viewModelScope.launch { + fetchNextPage() + } + } /** * Observing thread list with respect to homeserver capabilities. @@ -181,21 +189,23 @@ class ThreadListViewModel @AssistedInject constructor( true -> ThreadFilter.PARTICIPATED false -> ThreadFilter.ALL } - room?.threadsService()?.fetchThreadList( - nextBatchId = nextBatchId, - limit = defaultPagedListConfig.pageSize, - filter = filter, - ).let { result -> - when (result) { - is FetchThreadsResult.ReachedEnd -> { - hasReachedEnd = true - } - is FetchThreadsResult.ShouldFetchMore -> { - nextBatchId = result.nextBatch - } - else -> { + try { + room?.threadsService()?.fetchThreadList( + nextBatchId = nextBatchId, + limit = defaultPagedListConfig.pageSize, + filter = filter, + )?.let { result -> + when (result) { + is FetchThreadsResult.ReachedEnd -> { + hasReachedEnd = true + } + is FetchThreadsResult.ShouldFetchMore -> { + nextBatchId = result.nextBatch + } } } + } catch (throwable: Throwable) { + _viewEvents.post(ThreadListViewEvents.ShowError(throwable)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 318c250906..1e67941856 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -26,6 +26,7 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.cleanup @@ -41,10 +42,14 @@ import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewActions +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewEvents import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.ReportType +import org.matrix.android.sdk.api.failure.is400 +import org.matrix.android.sdk.api.failure.is404 import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem @@ -126,11 +131,45 @@ class ThreadListFragment : views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) legacyThreadListController.listener = this } + observeViewEvents() + } + + private fun observeViewEvents() { + threadListViewModel.observeViewEvents { + when (it) { + is ThreadListViewEvents.ShowError -> handleShowError(it) + } + } + } + + private fun handleShowError(event: ThreadListViewEvents.ShowError) { + val error = event.throwable + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .also { + if (error.is400() || error.is404()) { + // Outdated Homeserver + it.setMessage(R.string.thread_list_not_available) + it.setPositiveButton(R.string.ok) { _, _ -> + requireActivity().finish() + } + } else { + // Other error, can retry + // (Can happen on first request or on pagination request) + it.setMessage(errorFormatter.toHumanReadable(error)) + it.setPositiveButton(R.string.ok, null) + it.setNegativeButton(R.string.global_retry) { _, _ -> + threadListViewModel.handle(ThreadListViewActions.TryAgain) + } + } + } + .show() } override fun onDestroyView() { views.threadListRecyclerView.cleanup() threadListController.listener = null + legacyThreadListController.listener = null super.onDestroyView() }