From 100673aa9c05492d1514f227a8d738ca40df2a02 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 15 Jun 2023 19:59:30 +0200 Subject: [PATCH] Handle status edit histories with < 2 entries (#3747) This can happen if the edit history has not been propogated to the user's server. If the edit history is missing then show an error with a link to the specifc Mastodon issue. Fixes #3743 --- .../viewthread/edits/ViewEditsFragment.kt | 27 ++++- .../viewthread/edits/ViewEditsViewModel.kt | 112 ++++++++++-------- .../tusky/view/BackgroundMessageView.kt | 2 + .../res/layout/view_background_message.xml | 2 +- app/src/main/res/values/strings.xml | 3 +- 5 files changed, 85 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt index 3378b7a27..9fa5a30ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt @@ -111,13 +111,28 @@ class ViewEditsFragment : binding.statusView.show() binding.initialProgressBar.hide() - if (uiState.throwable is IOException) { - binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { - viewModel.loadEdits(statusId, force = true) + when (uiState.throwable) { + is IOException -> { + binding.statusView.setup( + R.drawable.elephant_offline, + R.string.error_network + ) { + viewModel.loadEdits(statusId, force = true) + } } - } else { - binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { - viewModel.loadEdits(statusId, force = true) + is ViewEditsViewModel.MissingEditsException -> { + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.error_missing_edits + ) + } + else -> { + binding.statusView.setup( + R.drawable.elephant_error, + R.string.error_generic + ) { + viewModel.loadEdits(statusId, force = true) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt index c1e76da3c..93f663587 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.viewthread.edits import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrElse import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.DELETED_TEXT_EL import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.INSERTED_TEXT_EL import com.keylesspalace.tusky.entity.StatusEdit @@ -48,6 +48,9 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie private val _uiState: MutableStateFlow = MutableStateFlow(EditsUiState.Initial) val uiState: StateFlow = _uiState.asStateFlow() + /** The API call to fetch edit history returned less than two items */ + object MissingEditsException : Exception() + fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) { if (!force && _uiState.value !is EditsUiState.Initial) return @@ -58,63 +61,68 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie } viewModelScope.launch { - api.statusEdits(statusId).fold( - { edits -> - // Diff each status' content against the previous version, producing new - // content with additional `ins` or `del` elements marking inserted or - // deleted content. - // - // This can be CPU intensive depending on the number of edits and the size - // of each, so don't run this on Dispatchers.Main. - viewModelScope.launch(Dispatchers.Default) { - val sortedEdits = edits.sortedBy { it.createdAt } - .reversed() - .toMutableList() + val edits = api.statusEdits(statusId).getOrElse { + _uiState.value = EditsUiState.Error(it) + return@launch + } - SAXLoader.setXMLReaderClass("org.xmlpull.v1.sax2.Driver") - val loader = SAXLoader() - loader.config = DiffConfig( - false, - WhiteSpaceProcessing.PRESERVE, - TextGranularity.SPACE_WORD + // `edits` might have fewer than the minimum number of entries because of + // https://github.com/mastodon/mastodon/issues/25398. + if (edits.size < 2) { + _uiState.value = EditsUiState.Error(MissingEditsException) + return@launch + } + + // Diff each status' content against the previous version, producing new + // content with additional `ins` or `del` elements marking inserted or + // deleted content. + // + // This can be CPU intensive depending on the number of edits and the size + // of each, so don't run this on Dispatchers.Main. + viewModelScope.launch(Dispatchers.Default) { + val sortedEdits = edits.sortedBy { it.createdAt } + .reversed() + .toMutableList() + + SAXLoader.setXMLReaderClass("org.xmlpull.v1.sax2.Driver") + val loader = SAXLoader() + loader.config = DiffConfig( + false, + WhiteSpaceProcessing.PRESERVE, + TextGranularity.SPACE_WORD + ) + val processor = OptimisticXMLProcessor() + processor.setCoalesce(true) + val output = HtmlDiffOutput() + + try { + // The XML processor expects `br` to be closed + var currentContent = + loader.load(sortedEdits[0].content.replace("
", "
")) + var previousContent = + loader.load(sortedEdits[1].content.replace("
", "
")) + + for (i in 1 until sortedEdits.size) { + processor.diff(previousContent, currentContent, output) + sortedEdits[i - 1] = sortedEdits[i - 1].copy( + content = output.xml.toString() ) - val processor = OptimisticXMLProcessor() - processor.setCoalesce(true) - val output = HtmlDiffOutput() - try { - // The XML processor expects `br` to be closed - var currentContent = - loader.load(sortedEdits[0].content.replace("
", "
")) - var previousContent = - loader.load(sortedEdits[1].content.replace("
", "
")) - - for (i in 1 until sortedEdits.size) { - processor.diff(previousContent, currentContent, output) - sortedEdits[i - 1] = sortedEdits[i - 1].copy( - content = output.xml.toString() - ) - - if (i < sortedEdits.size - 1) { - currentContent = previousContent - previousContent = loader.load( - sortedEdits[i + 1].content.replace("
", "
") - ) - } - } - _uiState.value = EditsUiState.Success(sortedEdits) - } catch (_: LoadingException) { - // Something failed parsing the XML from the server. Rather than - // show an error just return the sorted edits so the user can at - // least visually scan the differences. - _uiState.value = EditsUiState.Success(sortedEdits) + if (i < sortedEdits.size - 1) { + currentContent = previousContent + previousContent = loader.load( + sortedEdits[i + 1].content.replace("
", "
") + ) } } - }, - { throwable -> - _uiState.value = EditsUiState.Error(throwable) + _uiState.value = EditsUiState.Success(sortedEdits) + } catch (_: LoadingException) { + // Something failed parsing the XML from the server. Rather than + // show an error just return the sorted edits so the user can at + // least visually scan the differences. + _uiState.value = EditsUiState.Success(sortedEdits) } - ) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index 4ccd5627e..97078d502 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -1,6 +1,7 @@ package com.keylesspalace.tusky.view import android.content.Context +import android.text.method.LinkMovementMethod import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater @@ -44,6 +45,7 @@ class BackgroundMessageView @JvmOverloads constructor( clickListener: ((v: View) -> Unit)? = null ) { binding.messageTextView.setText(messageRes) + binding.messageTextView.movementMethod = LinkMovementMethod.getInstance() binding.imageView.setImageResource(imageRes) binding.button.setOnClickListener(clickListener) binding.button.visible(clickListener != null) diff --git a/app/src/main/res/layout/view_background_message.xml b/app/src/main/res/layout/view_background_message.xml index 65e9cc5ac..3c6e26cdd 100644 --- a/app/src/main/res/layout/view_background_message.xml +++ b/app/src/main/res/layout/view_background_message.xml @@ -35,7 +35,7 @@ android:scaleType="centerInside" android:src="@drawable/elephant_offline" /> - Oldest first Newest first - Edited: %1$s - Created: %1$s + Your server knows that this post was edited, but does not have a copy of the edits, so they can\'t be shown to you.\n\nThis is Mastodon issue #25398. Loading thread