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:
parent
c3ad055092
commit
100673aa9c
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
Loading…
Reference in New Issue