Rotates all versions of a note
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
@@ -1402,6 +1403,13 @@ object LocalCache {
|
|||||||
|
|
||||||
note.loadEvent(event, author, emptyList())
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
event.editedNote()?.let {
|
||||||
|
getNoteIfExists(it)?.let { editedNote ->
|
||||||
|
modificationCache.remove(editedNote.idHex)
|
||||||
|
editedNote.liveSet?.innerModifications?.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1716,15 +1724,31 @@ object LocalCache {
|
|||||||
return minTime
|
return minTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val modificationCache = LruCache<HexKey, List<Note>>(20)
|
||||||
|
|
||||||
|
fun cachedModificationEventsForNote(note: Note): List<Note>? {
|
||||||
|
return modificationCache[note.idHex]
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun findLatestModificationForNote(note: Note): List<Note> {
|
suspend fun findLatestModificationForNote(note: Note): List<Note> {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
modificationCache[note.idHex]?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
val time = TimeUtils.now()
|
val time = TimeUtils.now()
|
||||||
|
|
||||||
return noteListCache.filter { item ->
|
val newNotes =
|
||||||
val noteEvent = item.event
|
noteListCache.filter { item ->
|
||||||
|
val noteEvent = item.event
|
||||||
|
|
||||||
noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
noteEvent is TextNoteModificationEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
||||||
}
|
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||||
|
|
||||||
|
modificationCache.put(note.idHex, newNotes)
|
||||||
|
|
||||||
|
return newNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanObservers() {
|
fun cleanObservers() {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -1088,10 +1089,58 @@ fun InnerNoteWithReactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class EditState(
|
class EditState() {
|
||||||
val showOriginal: MutableState<Boolean> = mutableStateOf(false),
|
private var modificationsList: List<Note> = persistentListOf()
|
||||||
val modificationsInOrder: MutableState<List<Note>> = mutableStateOf(emptyList()),
|
private var modificationToShowIndex: Int = -1
|
||||||
)
|
|
||||||
|
val modificationToShow: MutableState<Note?> = mutableStateOf(null)
|
||||||
|
val showingVersion: MutableState<Int> = mutableStateOf(0)
|
||||||
|
|
||||||
|
fun hasModificationsToShow(): Boolean = modificationsList.isNotEmpty()
|
||||||
|
|
||||||
|
fun isOriginal(): Boolean = modificationToShowIndex < 0
|
||||||
|
|
||||||
|
fun isLatest(): Boolean = modificationToShowIndex == modificationsList.lastIndex
|
||||||
|
|
||||||
|
fun originalVersionId() = 0
|
||||||
|
|
||||||
|
fun lastVersionId() = modificationsList.size
|
||||||
|
|
||||||
|
fun versionId() = modificationToShowIndex + 1
|
||||||
|
|
||||||
|
fun nextModification() {
|
||||||
|
if (modificationToShowIndex < 0) {
|
||||||
|
modificationToShowIndex = 0
|
||||||
|
modificationToShow.value = modificationsList.getOrNull(0)
|
||||||
|
} else {
|
||||||
|
modificationToShowIndex++
|
||||||
|
if (modificationToShowIndex >= modificationsList.size) {
|
||||||
|
modificationToShowIndex = -1
|
||||||
|
modificationToShow.value = null
|
||||||
|
} else {
|
||||||
|
modificationToShow.value = modificationsList.getOrNull(modificationToShowIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showingVersion.value = versionId()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateModifications(newModifications: List<Note>) {
|
||||||
|
if (modificationsList != newModifications) {
|
||||||
|
modificationsList = newModifications
|
||||||
|
|
||||||
|
if (newModifications.isEmpty()) {
|
||||||
|
modificationToShow.value = null
|
||||||
|
modificationToShowIndex = -1
|
||||||
|
} else {
|
||||||
|
modificationToShowIndex = newModifications.lastIndex
|
||||||
|
modificationToShow.value = newModifications.last()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showingVersion.value = versionId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NoteBody(
|
private fun NoteBody(
|
||||||
@@ -1105,14 +1154,7 @@ private fun NoteBody(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val editState by
|
val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel)
|
||||||
produceState(initialValue = EditState(), key1 = baseNote) {
|
|
||||||
accountViewModel.findModificationEventsForNote(baseNote) { newModifications ->
|
|
||||||
if (value.modificationsInOrder.value != newModifications) {
|
|
||||||
value.modificationsInOrder.value = newModifications
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FirstUserInfoRow(
|
FirstUserInfoRow(
|
||||||
baseNote = baseNote,
|
baseNote = baseNote,
|
||||||
@@ -1168,7 +1210,7 @@ private fun RenderNoteRow(
|
|||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
makeItShort: Boolean,
|
makeItShort: Boolean,
|
||||||
canPreview: Boolean,
|
canPreview: Boolean,
|
||||||
editState: EditState,
|
editState: State<GenericLoadable<EditState>>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -1358,7 +1400,7 @@ fun RenderTextEvent(
|
|||||||
makeItShort: Boolean,
|
makeItShort: Boolean,
|
||||||
canPreview: Boolean,
|
canPreview: Boolean,
|
||||||
backgroundColor: MutableState<Color>,
|
backgroundColor: MutableState<Color>,
|
||||||
editState: EditState,
|
editState: State<GenericLoadable<EditState>>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -1371,10 +1413,10 @@ fun RenderTextEvent(
|
|||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||||
val newBody =
|
val newBody =
|
||||||
if (editState.showOriginal.value || editState.modificationsInOrder.value.isEmpty()) {
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
body
|
(editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value?.event?.content() ?: body
|
||||||
} else {
|
} else {
|
||||||
editState.modificationsInOrder.value.firstOrNull()?.event?.content() ?: body
|
body
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) {
|
if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) {
|
||||||
@@ -2822,7 +2864,7 @@ fun DisplayLocation(
|
|||||||
fun FirstUserInfoRow(
|
fun FirstUserInfoRow(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
showAuthorPicture: Boolean,
|
showAuthorPicture: Boolean,
|
||||||
editState: EditState,
|
editState: State<GenericLoadable<EditState>>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -2857,8 +2899,10 @@ fun FirstUserInfoRow(
|
|||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editState.modificationsInOrder.value.isEmpty()) {
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
DisplayEditStatus(editState.showOriginal)
|
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
|
||||||
|
DisplayEditStatus(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeAgo(baseNote)
|
TimeAgo(baseNote)
|
||||||
@@ -2868,15 +2912,69 @@ fun FirstUserInfoRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayEditStatus(showOriginal: MutableState<Boolean>) {
|
fun observeEdits(
|
||||||
|
baseNote: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
): State<GenericLoadable<EditState>> {
|
||||||
|
val editState =
|
||||||
|
remember(baseNote.idHex) {
|
||||||
|
val cached = accountViewModel.cachedModificationEventsForNote(baseNote)
|
||||||
|
mutableStateOf(
|
||||||
|
if (cached != null) {
|
||||||
|
if (cached.isEmpty()) {
|
||||||
|
GenericLoadable.Empty<EditState>()
|
||||||
|
} else {
|
||||||
|
val state = EditState()
|
||||||
|
state.updateModifications(cached)
|
||||||
|
GenericLoadable.Loaded<EditState>(state)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GenericLoadable.Loading<EditState>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedNote = baseNote.live().innerModifications.observeAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = updatedNote) {
|
||||||
|
updatedNote.value?.note?.let {
|
||||||
|
accountViewModel.findModificationEventsForNote(it) { newModifications ->
|
||||||
|
if (newModifications.isEmpty()) {
|
||||||
|
if (editState.value !is GenericLoadable.Empty) {
|
||||||
|
editState.value = GenericLoadable.Empty<EditState>()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
|
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.updateModifications(newModifications)
|
||||||
|
} else {
|
||||||
|
val state = EditState()
|
||||||
|
state.updateModifications(newModifications)
|
||||||
|
editState.value = GenericLoadable.Loaded(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return editState
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayEditStatus(editState: EditState) {
|
||||||
ClickableText(
|
ClickableText(
|
||||||
text =
|
text =
|
||||||
if (showOriginal.value) {
|
buildAnnotatedString {
|
||||||
buildAnnotatedString { append(stringResource(id = R.string.original)) }
|
if (editState.showingVersion.value == editState.originalVersionId()) {
|
||||||
} else {
|
append(stringResource(id = R.string.original))
|
||||||
buildAnnotatedString { append(stringResource(id = R.string.edited)) }
|
} else if (editState.showingVersion.value == editState.lastVersionId()) {
|
||||||
|
append(stringResource(id = R.string.edited))
|
||||||
|
} else {
|
||||||
|
append(stringResource(id = R.string.edited_number, editState.versionId()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = { showOriginal.value = !showOriginal.value },
|
onClick = {
|
||||||
|
editState.nextModification()
|
||||||
|
},
|
||||||
style =
|
style =
|
||||||
LocalTextStyle.current.copy(
|
LocalTextStyle.current.copy(
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -86,6 +85,7 @@ import coil.compose.AsyncImage
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
@@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPostApproval
|
|||||||
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||||
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
|
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.VideoDisplay
|
import com.vitorpamplona.amethyst.ui.note.VideoDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.observeEdits
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -377,20 +378,13 @@ fun NoteMaster(
|
|||||||
onClick = { showHiddenNote = true },
|
onClick = { showHiddenNote = true },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val editState by
|
|
||||||
produceState(initialValue = EditState(), key1 = baseNote) {
|
|
||||||
accountViewModel.findModificationEventsForNote(baseNote) { newModifications ->
|
|
||||||
if (value.modificationsInOrder.value != newModifications) {
|
|
||||||
value.modificationsInOrder.value = newModifications
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 10.dp),
|
.padding(top = 10.dp),
|
||||||
) {
|
) {
|
||||||
|
val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -421,8 +415,10 @@ fun NoteMaster(
|
|||||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editState.modificationsInOrder.value.isEmpty()) {
|
if (editState.value is GenericLoadable.Loaded) {
|
||||||
DisplayEditStatus(editState.showOriginal)
|
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
|
||||||
|
DisplayEditStatus(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -850,9 +846,9 @@ private fun RenderWikiHeaderForThreadPreview() {
|
|||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav: (String) -> Unit = {}
|
val nav: (String) -> Unit = {}
|
||||||
|
|
||||||
val editState by
|
val editState =
|
||||||
remember {
|
remember {
|
||||||
mutableStateOf(EditState())
|
mutableStateOf(GenericLoadable.Empty<EditState>())
|
||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
@@ -932,6 +932,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note)
|
||||||
|
|
||||||
suspend fun findModificationEventsForNote(
|
suspend fun findModificationEventsForNote(
|
||||||
note: Note,
|
note: Note,
|
||||||
onResult: (List<Note>) -> Unit,
|
onResult: (List<Note>) -> Unit,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
<string name="boost">Boost</string>
|
<string name="boost">Boost</string>
|
||||||
<string name="boosted">boosted</string>
|
<string name="boosted">boosted</string>
|
||||||
<string name="edited">edited</string>
|
<string name="edited">edited</string>
|
||||||
|
<string name="edited_number">edit #%1$s</string>
|
||||||
<string name="original">original</string>
|
<string name="original">original</string>
|
||||||
<string name="quote">Quote</string>
|
<string name="quote">Quote</string>
|
||||||
<string name="fork">Fork</string>
|
<string name="fork">Fork</string>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class TextNoteModificationEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun editedNote() = firstTaggedEvent()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 1010
|
const val KIND = 1010
|
||||||
const val ALT = "Content Change Event"
|
const val ALT = "Content Change Event"
|
||||||
|
|||||||
Reference in New Issue
Block a user