diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 2d1d84a2d..039090ac7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1467,6 +1467,8 @@ class Account( fun sendEdit( message: String, originalNote: Note, + notify: HexKey?, + summary: String? = null, relayList: List? = null, ) { if (!isWriteable()) return @@ -1476,6 +1478,8 @@ class Account( TextNoteModificationEvent.create( content = message, eventId = idHex, + notify = notify, + summary = summary, signer = signer, ) { LocalCache.justConsume(it, null) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index e389b7abc..177d858cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview 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.UserLine 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.Size5dp 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.replyModifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -119,6 +122,7 @@ fun EditPostView( nav: (String) -> Unit, ) { val postViewModel: EditPostViewModel = viewModel() + postViewModel.prepare(edit, versionLookingAt, accountViewModel) val context = LocalContext.current @@ -257,6 +261,21 @@ fun EditPostView( .fillMaxWidth() .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) 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, + ), + ) + } + }*/ } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 13c188653..b2439727d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -59,6 +59,8 @@ open class EditPostViewModel() : ViewModel() { var editedFromNote: Note? = null + var subject by mutableStateOf(TextFieldValue("")) + var nip94attachments by mutableStateOf>(emptyList()) var nip95attachments by mutableStateOf>>(emptyList()) @@ -80,6 +82,16 @@ open class EditPostViewModel() : ViewModel() { var canAddInvoice 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( edit: Note, versionLookingAt: Note?, @@ -111,15 +123,29 @@ open class EditPostViewModel() : ViewModel() { 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( message = message.text, originalNote = editedFromNote!!, + notify = notify, + summary = subject.text.ifBlank { null }, relayList = relayList, ) cancel() } + open fun updateSubject(it: TextFieldValue) { + subject = it + } + fun upload( galleryUri: Uri, alt: String?, @@ -192,6 +218,7 @@ open class EditPostViewModel() : ViewModel() { open fun cancel() { message = TextFieldValue("") + subject = TextFieldValue("") editedFromNote = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 50394e144..9b57df413 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -48,6 +48,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider 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.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight 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.User 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.components.ClickableUrl 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.grayText 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.mediumImportanceLink 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.RepostEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent @@ -291,8 +297,7 @@ fun NoteCompose( nav = nav, ) } else { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup, - -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> BlankNote( remember { modifier.combinedClickable( @@ -1111,6 +1116,8 @@ class EditState() { fun versionId() = modificationToShowIndex + 1 + fun latest() = modificationsList.lastOrNull() + fun nextModification() { if (modificationToShowIndex < 0) { modificationToShowIndex = 0 @@ -1339,6 +1346,17 @@ private fun RenderNoteRow( nav, ) } + is TextNoteModificationEvent -> { + RenderTextModificationEvent( + baseNote, + makeItShort, + canPreview, + backgroundColor, + editState, + accountViewModel, + nav, + ) + } else -> { RenderTextEvent( baseNote, @@ -1467,6 +1485,124 @@ fun RenderTextEvent( } } +@Composable +fun RenderTextModificationEvent( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + editStateByAuthor: State>, + 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 + + 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 fun RenderPoll( note: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 04c1cd08b..50f77676d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -581,13 +581,22 @@ fun NoteDropDownMenu( }, ) Divider() - if (state.isLoggedUser && note.event is TextNoteEvent) { - DropdownMenuItem( - text = { Text(stringResource(R.string.edit_post)) }, - onClick = { - wantsToEditPost.value = true - }, - ) + if (note.event is TextNoteEvent) { + if (state.isLoggedUser) { + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_post)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.propose_an_edit)) }, + onClick = { + wantsToEditPost.value = true + }, + ) + } } DropdownMenuItem( text = { Text(stringResource(R.string.broadcast)) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 2ff8f03c4..97b2c8258 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderRepost 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.observeEdits 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.RelaySetEvent import com.vitorpamplona.quartz.events.RepostEvent +import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.WikiNoteEvent import kotlinx.collections.immutable.toImmutableList @@ -568,6 +570,16 @@ fun NoteMaster( ) } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { 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) { val canPreview = note.author == account.userProfile() || diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74e89ae60..845fe0b15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ original Quote Fork + Propose an Edit New Amount in Sats Add "replying to " @@ -795,4 +796,9 @@ There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. Edit Post + Proposal to improve your post + Summary of changes + Quick fixes... + + Accept the Suggestion diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt index c3dd0a9ac..0094c5983 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteModificationEvent.kt @@ -36,6 +36,8 @@ class TextNoteModificationEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun editedNote() = firstTaggedEvent() + fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + companion object { const val KIND = 1010 const val ALT = "Content Change Event" @@ -43,12 +45,25 @@ class TextNoteModificationEvent( fun create( content: String, eventId: HexKey, + notify: HexKey?, + summary: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (TextNoteModificationEvent) -> Unit, ) { - val tags = arrayOf(arrayOf("e", eventId), arrayOf("alt", ALT)) - signer.sign(createdAt, KIND, tags, content, onReady) + val tags = mutableListOf(arrayOf("e", eventId)) + + 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) } } }