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
This commit is contained in:
Nik Clayton 2023-06-15 19:59:30 +02:00 committed by GitHub
parent c3ad055092
commit 100673aa9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 61 deletions

View File

@ -111,13 +111,28 @@ class ViewEditsFragment :
binding.statusView.show() binding.statusView.show()
binding.initialProgressBar.hide() binding.initialProgressBar.hide()
if (uiState.throwable is IOException) { when (uiState.throwable) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { is IOException -> {
viewModel.loadEdits(statusId, force = true) binding.statusView.setup(
R.drawable.elephant_offline,
R.string.error_network
) {
viewModel.loadEdits(statusId, force = true)
}
} }
} else { is ViewEditsViewModel.MissingEditsException -> {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.statusView.setup(
viewModel.loadEdits(statusId, force = true) 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)
}
} }
} }
} }

View File

@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.viewthread.edits
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.DELETED_TEXT_EL
import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.INSERTED_TEXT_EL import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.INSERTED_TEXT_EL
import com.keylesspalace.tusky.entity.StatusEdit import com.keylesspalace.tusky.entity.StatusEdit
@ -48,6 +48,9 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie
private val _uiState: MutableStateFlow<EditsUiState> = MutableStateFlow(EditsUiState.Initial) private val _uiState: MutableStateFlow<EditsUiState> = MutableStateFlow(EditsUiState.Initial)
val uiState: StateFlow<EditsUiState> = _uiState.asStateFlow() val uiState: StateFlow<EditsUiState> = _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) { fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) {
if (!force && _uiState.value !is EditsUiState.Initial) return if (!force && _uiState.value !is EditsUiState.Initial) return
@ -58,63 +61,68 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie
} }
viewModelScope.launch { viewModelScope.launch {
api.statusEdits(statusId).fold( val edits = api.statusEdits(statusId).getOrElse {
{ edits -> _uiState.value = EditsUiState.Error(it)
// Diff each status' content against the previous version, producing new return@launch
// 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") // `edits` might have fewer than the minimum number of entries because of
val loader = SAXLoader() // https://github.com/mastodon/mastodon/issues/25398.
loader.config = DiffConfig( if (edits.size < 2) {
false, _uiState.value = EditsUiState.Error(MissingEditsException)
WhiteSpaceProcessing.PRESERVE, return@launch
TextGranularity.SPACE_WORD }
// 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("<br>", "<br/>"))
var previousContent =
loader.load(sortedEdits[1].content.replace("<br>", "<br/>"))
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 { if (i < sortedEdits.size - 1) {
// The XML processor expects `br` to be closed currentContent = previousContent
var currentContent = previousContent = loader.load(
loader.load(sortedEdits[0].content.replace("<br>", "<br/>")) sortedEdits[i + 1].content.replace("<br>", "<br/>")
var previousContent = )
loader.load(sortedEdits[1].content.replace("<br>", "<br/>"))
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("<br>", "<br/>")
)
}
}
_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)
} }
} }
}, _uiState.value = EditsUiState.Success(sortedEdits)
{ throwable -> } catch (_: LoadingException) {
_uiState.value = EditsUiState.Error(throwable) // 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)
} }
) }
} }
} }

View File

@ -1,6 +1,7 @@
package com.keylesspalace.tusky.view package com.keylesspalace.tusky.view
import android.content.Context import android.content.Context
import android.text.method.LinkMovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -44,6 +45,7 @@ class BackgroundMessageView @JvmOverloads constructor(
clickListener: ((v: View) -> Unit)? = null clickListener: ((v: View) -> Unit)? = null
) { ) {
binding.messageTextView.setText(messageRes) binding.messageTextView.setText(messageRes)
binding.messageTextView.movementMethod = LinkMovementMethod.getInstance()
binding.imageView.setImageResource(imageRes) binding.imageView.setImageResource(imageRes)
binding.button.setOnClickListener(clickListener) binding.button.setOnClickListener(clickListener)
binding.button.visible(clickListener != null) binding.button.visible(clickListener != null)

View File

@ -35,7 +35,7 @@
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/elephant_offline" /> android:src="@drawable/elephant_offline" />
<TextView <com.keylesspalace.tusky.view.ClickableSpanTextView
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -764,10 +764,9 @@
<string name="pref_reading_order_oldest_first">Oldest first</string> <string name="pref_reading_order_oldest_first">Oldest first</string>
<string name="pref_reading_order_newest_first">Newest first</string> <string name="pref_reading_order_newest_first">Newest first</string>
<!--@Tusky edited 19th December 2022 13:37 -->
<string name="status_edit_info">Edited: %1$s</string> <string name="status_edit_info">Edited: %1$s</string>
<!--@Tusky created 19th December 2022 13:12 -->
<string name="status_created_info">Created: %1$s</string> <string name="status_created_info">Created: %1$s</string>
<string name="error_missing_edits">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 <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon issue #25398</a>.</string>
<string name="a11y_label_loading_thread">Loading thread</string> <string name="a11y_label_loading_thread">Loading thread</string>
<!--@knossos@fosstodon.org created 2023-01-07 --> <!--@knossos@fosstodon.org created 2023-01-07 -->