Visualizing proposed edits from other people

This commit is contained in:
Vitor Pamplona
2024-03-01 21:15:43 -05:00
parent 81cc985e3b
commit b694ac7259
8 changed files with 276 additions and 11 deletions
@@ -1467,6 +1467,8 @@ class Account(
fun sendEdit( fun sendEdit(
message: String, message: String,
originalNote: Note, originalNote: Note,
notify: HexKey?,
summary: String? = null,
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1476,6 +1478,8 @@ class Account(
TextNoteModificationEvent.create( TextNoteModificationEvent.create(
content = message, content = message,
eventId = idHex, eventId = idHex,
notify = notify,
summary = summary,
signer = signer, signer = signer,
) { ) {
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -101,7 +102,9 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -119,6 +122,7 @@ fun EditPostView(
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val postViewModel: EditPostViewModel = viewModel() val postViewModel: EditPostViewModel = viewModel()
postViewModel.prepare(edit, versionLookingAt, accountViewModel)
val context = LocalContext.current val context = LocalContext.current
@@ -257,6 +261,21 @@ fun EditPostView(
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(scrollState), .verticalScroll(scrollState),
) { ) {
postViewModel.editedFromNote?.let {
Row(Modifier.heightIn(max = 200.dp)) {
NoteCompose(
baseNote = it,
makeItShort = true,
unPackReply = false,
isQuotedNote = true,
modifier = MaterialTheme.colorScheme.replyModifier,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdVertSpacer)
}
}
MessageField(postViewModel) MessageField(postViewModel)
val myUrlPreview = postViewModel.urlPreview val myUrlPreview = postViewModel.urlPreview
@@ -353,6 +372,43 @@ fun EditPostView(
} }
} }
} }
/*
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp, horizontal = Size10dp),
) {
Column {
Text(
text = stringResource(R.string.message_to_author),
fontSize = 18.sp,
fontWeight = FontWeight.W500,
)
Divider()
MyTextField(
value = postViewModel.subject,
onValueChange = { postViewModel.updateSubject(it) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringResource(R.string.message_to_author_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
visualTransformation =
UrlUserTagTransformation(
MaterialTheme.colorScheme.primary,
),
colors =
OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
),
)
}
}*/
} }
} }
@@ -59,6 +59,8 @@ open class EditPostViewModel() : ViewModel() {
var editedFromNote: Note? = null var editedFromNote: Note? = null
var subject by mutableStateOf(TextFieldValue(""))
var nip94attachments by mutableStateOf<List<FileHeaderEvent>>(emptyList()) var nip94attachments by mutableStateOf<List<FileHeaderEvent>>(emptyList())
var nip95attachments by var nip95attachments by
mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList()) mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
@@ -80,6 +82,16 @@ open class EditPostViewModel() : ViewModel() {
var canAddInvoice by mutableStateOf(false) var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false)
open fun prepare(
edit: Note,
versionLookingAt: Note?,
accountViewModel: AccountViewModel,
) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
this.editedFromNote = edit
}
open fun load( open fun load(
edit: Note, edit: Note,
versionLookingAt: Note?, versionLookingAt: Note?,
@@ -111,15 +123,29 @@ open class EditPostViewModel() : ViewModel() {
account?.sendNip95(it.first, it.second, relayList) account?.sendNip95(it.first, it.second, relayList)
} }
val notify =
if (editedFromNote?.author?.pubkeyHex == account?.userProfile()?.pubkeyHex) {
null
} else {
// notifies if it is not the logged in user
editedFromNote?.author?.pubkeyHex
}
account?.sendEdit( account?.sendEdit(
message = message.text, message = message.text,
originalNote = editedFromNote!!, originalNote = editedFromNote!!,
notify = notify,
summary = subject.text.ifBlank { null },
relayList = relayList, relayList = relayList,
) )
cancel() cancel()
} }
open fun updateSubject(it: TextFieldValue) {
subject = it
}
fun upload( fun upload(
galleryUri: Uri, galleryUri: Uri,
alt: String?, alt: String?,
@@ -192,6 +218,7 @@ open class EditPostViewModel() : ViewModel() {
open fun cancel() { open fun cancel() {
message = TextFieldValue("") message = TextFieldValue("")
subject = TextFieldValue("")
editedFromNote = null editedFromNote = null
@@ -48,6 +48,8 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -85,6 +87,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -114,6 +117,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.CachedGeoLocations import com.vitorpamplona.amethyst.service.CachedGeoLocations
import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
@@ -183,6 +187,7 @@ import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
@@ -235,6 +240,7 @@ import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
@@ -291,8 +297,7 @@ fun NoteCompose(
nav = nav, nav = nav,
) )
} else { } else {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup, LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
->
BlankNote( BlankNote(
remember { remember {
modifier.combinedClickable( modifier.combinedClickable(
@@ -1111,6 +1116,8 @@ class EditState() {
fun versionId() = modificationToShowIndex + 1 fun versionId() = modificationToShowIndex + 1
fun latest() = modificationsList.lastOrNull()
fun nextModification() { fun nextModification() {
if (modificationToShowIndex < 0) { if (modificationToShowIndex < 0) {
modificationToShowIndex = 0 modificationToShowIndex = 0
@@ -1339,6 +1346,17 @@ private fun RenderNoteRow(
nav, nav,
) )
} }
is TextNoteModificationEvent -> {
RenderTextModificationEvent(
baseNote,
makeItShort,
canPreview,
backgroundColor,
editState,
accountViewModel,
nav,
)
}
else -> { else -> {
RenderTextEvent( RenderTextEvent(
baseNote, baseNote,
@@ -1467,6 +1485,124 @@ fun RenderTextEvent(
} }
} }
@Composable
fun RenderTextModificationEvent(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: MutableState<Color>,
editStateByAuthor: State<GenericLoadable<EditState>>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteEvent = note.event as? TextNoteModificationEvent ?: return
val noteAuthor = note.author ?: return
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
val editState =
remember {
derivedStateOf {
val loadable = editStateByAuthor.value as? GenericLoadable.Loaded<EditState>
val state = EditState()
val latestChangeByAuthor =
if (loadable != null && loadable.loaded.hasModificationsToShow()) {
loadable.loaded.latest()
} else {
null
}
state.updateModifications(listOfNotNull(latestChangeByAuthor, note))
GenericLoadable.Loaded(state)
}
}
val wantsToEditPost =
remember {
mutableStateOf(false)
}
Card(
modifier = MaterialTheme.colorScheme.imageModifier,
) {
Column(Modifier.fillMaxWidth().padding(Size10dp)) {
Text(
text = stringResource(id = R.string.proposal_to_edit),
style =
TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
),
)
Spacer(modifier = StdVertSpacer)
noteEvent.summary()?.let {
TranslatableRichTextViewer(
content = it,
canPreview = canPreview && !makeItShort,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdVertSpacer)
}
noteEvent.editedNote()?.let {
LoadNote(baseNoteHex = it, accountViewModel = accountViewModel) { baseNote ->
baseNote?.let {
Column(
modifier =
MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp).clickable {
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
},
) {
NoteBody(
baseNote = baseNote,
showAuthorPicture = true,
unPackReply = false,
makeItShort = false,
canPreview = true,
showSecondRow = false,
backgroundColor = backgroundColor,
editState = editState,
accountViewModel = accountViewModel,
nav = nav,
)
if (wantsToEditPost.value) {
EditPostView(
onClose = {
wantsToEditPost.value = false
},
edit = baseNote,
versionLookingAt = note,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
}
}
Spacer(modifier = StdVertSpacer)
Button(
onClick = { wantsToEditPost.value = true },
modifier = Modifier.fillMaxWidth(),
) {
Text(text = stringResource(id = R.string.accept_the_suggestion))
}
}
}
}
@Composable @Composable
fun RenderPoll( fun RenderPoll(
note: Note, note: Note,
@@ -581,13 +581,22 @@ fun NoteDropDownMenu(
}, },
) )
Divider() Divider()
if (state.isLoggedUser && note.event is TextNoteEvent) { if (note.event is TextNoteEvent) {
if (state.isLoggedUser) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.edit_post)) }, text = { Text(stringResource(R.string.edit_post)) },
onClick = { onClick = {
wantsToEditPost.value = true wantsToEditPost.value = true
}, },
) )
} else {
DropdownMenuItem(
text = { Text(stringResource(R.string.propose_an_edit)) },
onClick = {
wantsToEditPost.value = true
},
)
}
} }
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.broadcast)) }, text = { Text(stringResource(R.string.broadcast)) },
@@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll
import com.vitorpamplona.amethyst.ui.note.RenderPostApproval 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.RenderTextModificationEvent
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.observeEdits
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmount
@@ -174,6 +175,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -568,6 +570,16 @@ fun NoteMaster(
) )
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
RenderRepost(baseNote, backgroundColor, accountViewModel, nav) RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is TextNoteModificationEvent) {
RenderTextModificationEvent(
note = baseNote,
makeItShort = false,
canPreview = true,
backgroundColor,
editState,
accountViewModel,
nav,
)
} else if (noteEvent is PollNoteEvent) { } else if (noteEvent is PollNoteEvent) {
val canPreview = val canPreview =
note.author == account.userProfile() || note.author == account.userProfile() ||
+6
View File
@@ -55,6 +55,7 @@
<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>
<string name="propose_an_edit">Propose an Edit</string>
<string name="new_amount_in_sats">New Amount in Sats</string> <string name="new_amount_in_sats">New Amount in Sats</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="replying_to">"replying to "</string> <string name="replying_to">"replying to "</string>
@@ -795,4 +796,9 @@
<string name="ots_info_description">There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time.</string> <string name="ots_info_description">There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time.</string>
<string name="edit_post">Edit Post</string> <string name="edit_post">Edit Post</string>
<string name="proposal_to_edit">Proposal to improve your post</string>
<string name="message_to_author">Summary of changes</string>
<string name="message_to_author_placeholder">Quick fixes...</string>
<string name="accept_the_suggestion">Accept the Suggestion</string>
</resources> </resources>
@@ -36,6 +36,8 @@ class TextNoteModificationEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun editedNote() = firstTaggedEvent() fun editedNote() = firstTaggedEvent()
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
companion object { companion object {
const val KIND = 1010 const val KIND = 1010
const val ALT = "Content Change Event" const val ALT = "Content Change Event"
@@ -43,12 +45,25 @@ class TextNoteModificationEvent(
fun create( fun create(
content: String, content: String,
eventId: HexKey, eventId: HexKey,
notify: HexKey?,
summary: String?,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (TextNoteModificationEvent) -> Unit, onReady: (TextNoteModificationEvent) -> Unit,
) { ) {
val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", ALT)) val tags = mutableListOf(arrayOf("e", eventId))
signer.sign(createdAt, KIND, tags, content, onReady)
notify?.let {
tags.add(arrayOf("p", it))
}
summary?.let {
tags.add(arrayOf("summary", it))
}
tags.add(arrayOf("alt", ALT))
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
} }
} }
} }