Merge pull request #7913 from vector-im/feature/bma/threadListCrashes

Thread list crashes
This commit is contained in:
Benoit Marty 2023-01-09 17:00:06 +01:00 committed by GitHub
commit ffb11d6455
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 19 deletions

1
changelog.d/7913.bugfix Normal file
View file

@ -0,0 +1 @@
Handle network error on API `rooms/{roomId}/threads`

View file

@ -794,6 +794,7 @@
<string name="thread_list_modal_my_threads_subtitle">Shows all threads youve participated in</string> <string name="thread_list_modal_my_threads_subtitle">Shows all threads youve participated in</string>
<string name="thread_list_empty_title">Keep discussions organized with threads</string> <string name="thread_list_empty_title">Keep discussions organized with threads</string>
<string name="thread_list_empty_subtitle">Threads help keep your conversations on-topic and easy to track.</string> <string name="thread_list_empty_subtitle">Threads help keep your conversations on-topic and easy to track.</string>
<string name="thread_list_not_available">You\'re homeserver does not support listing threads yet.</string>
<!-- Parameter %s will be replaced by the value of string reply_in_thread --> <!-- Parameter %s will be replaced by the value of string reply_in_thread -->
<string name="thread_list_empty_notice">Tip: Long tap a message and use “%s”.</string> <string name="thread_list_empty_notice">Tip: Long tap a message and use “%s”.</string>
<string name="search_thread_from_a_thread">From a Thread</string> <string name="search_thread_from_a_thread">From a Thread</string>

View file

@ -25,6 +25,9 @@ import java.io.IOException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun Throwable.is400() = this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_BAD_REQUEST
fun Throwable.is401() = this is Failure.ServerError && fun Throwable.is401() = this is Failure.ServerError &&
httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
error.code == MatrixError.M_UNAUTHORIZED error.code == MatrixError.M_UNAUTHORIZED

View file

@ -19,5 +19,4 @@ package org.matrix.android.sdk.api.session.room.threads
sealed class FetchThreadsResult { sealed class FetchThreadsResult {
data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult()
object ReachedEnd : FetchThreadsResult() object ReachedEnd : FetchThreadsResult()
object Failed : FetchThreadsResult()
} }

View file

@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
* This interface defines methods to interact with thread related features. * This interface defines methods to interact with thread related features.
* It's the dynamic threads implementation and the homeserver must return * It's the dynamic threads implementation and the homeserver must return
* a capability entry for threads. If the server do not support m.thread * a capability entry for threads. If the server do not support m.thread
* then [ThreadsLocalService] should be used instead * then [org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService] should be used instead
*/ */
interface ThreadsService { interface ThreadsService {

View file

@ -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
}

View file

@ -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
}

View file

@ -27,8 +27,6 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject 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.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
@ -52,7 +50,7 @@ class ThreadListViewModel @AssistedInject constructor(
@Assisted val initialState: ThreadListViewState, @Assisted val initialState: ThreadListViewState,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val session: Session, private val session: Session,
) : VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<ThreadListViewState, ThreadListViewActions, ThreadListViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId) private val room = session.getRoom(initialState.roomId)
@ -93,7 +91,17 @@ class ThreadListViewModel @AssistedInject constructor(
fetchAndObserveThreads() 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. * Observing thread list with respect to homeserver capabilities.
@ -181,11 +189,12 @@ class ThreadListViewModel @AssistedInject constructor(
true -> ThreadFilter.PARTICIPATED true -> ThreadFilter.PARTICIPATED
false -> ThreadFilter.ALL false -> ThreadFilter.ALL
} }
try {
room?.threadsService()?.fetchThreadList( room?.threadsService()?.fetchThreadList(
nextBatchId = nextBatchId, nextBatchId = nextBatchId,
limit = defaultPagedListConfig.pageSize, limit = defaultPagedListConfig.pageSize,
filter = filter, filter = filter,
).let { result -> )?.let { result ->
when (result) { when (result) {
is FetchThreadsResult.ReachedEnd -> { is FetchThreadsResult.ReachedEnd -> {
hasReachedEnd = true hasReachedEnd = true
@ -193,9 +202,10 @@ class ThreadListViewModel @AssistedInject constructor(
is FetchThreadsResult.ShouldFetchMore -> { is FetchThreadsResult.ShouldFetchMore -> {
nextBatchId = result.nextBatch nextBatchId = result.nextBatch
} }
else -> { }
} }
} } catch (throwable: Throwable) {
_viewEvents.post(ThreadListViewEvents.ShowError(throwable))
} }
} }
} }

View file

@ -26,6 +26,7 @@ import androidx.core.view.isVisible
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup 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.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController 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.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.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.rageshake.ReportType 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.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@ -126,11 +131,45 @@ class ThreadListFragment :
views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false)
legacyThreadListController.listener = this 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() { override fun onDestroyView() {
views.threadListRecyclerView.cleanup() views.threadListRecyclerView.cleanup()
threadListController.listener = null threadListController.listener = null
legacyThreadListController.listener = null
super.onDestroyView() super.onDestroyView()
} }