From c4c73a2bb700d60c95e8da404df9259e0309d8e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 18:05:59 -0400 Subject: [PATCH] Adds expirations in all new post screens. Adds expirations in the feed and chats. --- .../amethyst/ui/note/NoteCompose.kt | 39 ++++++++++++++++ .../expiration/ExpirationDatePicker.kt | 3 +- .../note/creators/expiration/IExpiration.kt | 28 +++++++++++ .../nip22Comments/CommentPostViewModel.kt | 26 ++++++++++- .../nip22Comments/GenericCommentPostScreen.kt | 15 ++++++ .../loggedIn/chats/feed/ChatMessageCompose.kt | 1 + .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 46 +++++++++++++++++++ .../privateDM/send/ChatNewMessageViewModel.kt | 38 ++++++++++----- .../chats/privateDM/send/NewGroupDMScreen.kt | 10 ++++ .../send/PrivateMessageEditFieldRow.kt | 21 ++++----- .../send/ChannelNewMessageViewModel.kt | 31 ++++++++++++- .../nip99Classifieds/NewProductScreen.kt | 15 ++++++ .../nip99Classifieds/NewProductViewModel.kt | 25 +++++++++- .../loggedIn/home/ShortNotePostViewModel.kt | 6 ++- .../publicMessages/NewPublicMessageScreen.kt | 15 ++++++ .../NewPublicMessageViewModel.kt | 25 +++++++++- .../loggedIn/threadview/ThreadFeedView.kt | 3 ++ amethyst/src/main/res/values/strings.xml | 1 + 18 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index e85bb5caf5..42ab09d74f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -30,6 +30,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -47,6 +51,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -201,6 +206,7 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent @@ -1344,6 +1350,26 @@ fun SecondUserInfoRow( } } +@Composable +fun DisplayExpiration(expirationDate: Long) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.expiration_date_label), + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + val context = LocalContext.current + Text( + text = timeAheadNoDot(expirationDate, context), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + } +} + @Composable fun DisplayOtsIfInOriginal( note: Note, @@ -1440,6 +1466,8 @@ fun FirstUserInfoRow( DisplayDraft() } + Expiration(baseNote) + TimeAgo(baseNote) if (moreOptions == null) { @@ -1450,6 +1478,17 @@ fun FirstUserInfoRow( } } +@Composable +fun Expiration(note: Note) { + val event = note.event + if (event != null) { + val expires = remember(event) { event.expiration() } + if (expires != null) { + DisplayExpiration(expires) + } + } +} + @Composable fun CheckAndDisplayEditStatus(editState: State>) { if (editState.value is GenericLoadable.Loaded) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 19aeca57f2..967f410586 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -69,7 +68,7 @@ import java.time.ZoneOffset @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ExpirationDatePicker(model: ShortNotePostViewModel) { +fun ExpirationDatePicker(model: IExpiration) { var showDatePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt new file mode 100644 index 0000000000..6d3a77fdbf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.creators.expiration + +import androidx.compose.runtime.Stable + +@Stable +interface IExpiration { + var expirationDate: Long +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index a885e3ce1b..c60b0a42e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -84,6 +86,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -101,6 +104,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -113,7 +117,8 @@ open class CommentPostViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() init { @@ -169,6 +174,10 @@ open class CommentPostViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -274,6 +283,10 @@ open class CommentPostViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val zapraiser = draftEvent.zapraiserAmount() wantsZapraiser = zapraiser != null zapRaiserAmount.value = null @@ -363,6 +376,7 @@ open class CommentPostViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val replyingTo = replyingTo val replyingToEvent = replyingTo?.event @@ -399,6 +413,7 @@ open class CommentPostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -419,6 +434,7 @@ open class CommentPostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -703,6 +719,14 @@ open class CommentPostViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationFlow(): StateFlow { if (location == null) { location = locationManager().geohashStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 4eae653ac4..ed922a496f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -61,6 +61,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -262,6 +264,15 @@ private fun GenericCommentPostBody( } } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { Row( verticalAlignment = CenterVertically, @@ -420,6 +431,10 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index ffef3d53a0..3aeb9d13ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -204,6 +204,7 @@ fun NormalChatNote( ) { IncognitoBadge(note) ChatTimeAgo(note) + ChatExpiration(note) RelayBadgesHorizontal(note, accountViewModel, nav = nav) Spacer(modifier = DoubleHorzSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index e53a80841b..cab7cf7521 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -20,16 +20,29 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgoShort +import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip40Expiration.expiration @Composable fun ChatTimeAgo(baseNote: Note) { @@ -43,3 +56,36 @@ fun ChatTimeAgo(baseNote: Note) { maxLines = 1, ) } + +@Composable +fun ChatExpiration(note: Note) { + val event = note.event + if (event != null) { + val expires = remember(event) { event.expiration() } + if (expires != null) { + ChatDisplayExpiration(expires) + } + } +} + +@Composable +fun ChatDisplayExpiration(expirationDate: Long) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.expiration_date_label), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + val context = LocalContext.current + Text( + text = timeAheadNoDot(expirationDate, context), + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font12SP, + maxLines = 1, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 61613f21f1..747920b86f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -42,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -104,7 +106,8 @@ class ChatNewMessageViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -163,6 +166,10 @@ class ChatNewMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -172,8 +179,6 @@ class ChatNewMessageViewModel : var wantsZapraiser by mutableStateOf(false) override var zapRaiserAmount = mutableStateOf(null) - var expirationDays by mutableStateOf(null) - // NIP17 Wrapped DMs / Group messages var nip17 by mutableStateOf(false) @@ -274,7 +279,8 @@ class ChatNewMessageViewModel : } fun loadExpiration(expirationDays: Int) { - this.expirationDays = expirationDays + this.wantsExpirationDate = true + this.expirationDate = TimeUtils.now() + expirationDays * 86400L } private fun loadFromDraft(draft: Note) { @@ -296,6 +302,10 @@ class ChatNewMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -348,8 +358,6 @@ class ChatNewMessageViewModel : } urlPreviews.update(message) - expirationDays = draftEvent.expiration()?.let { (it / 86_400).toInt() } - iMetaAttachments.addAll(draftEvent.imetas()) requiresNIP17 = draftEvent is NIP17Group @@ -444,11 +452,10 @@ class ChatNewMessageViewModel : val message = message.text val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null - val expiration = expirationDays?.let { TimeUtils.now() + it.toLong() * 86_400 } - if (nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) { val replyHint = replyTo.value?.toEventHint() @@ -463,7 +470,7 @@ class ChatNewMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -478,7 +485,7 @@ class ChatNewMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -501,7 +508,7 @@ class ChatNewMessageViewModel : replyingTo = replyTo.value?.toEventHint(), signer = accountViewModel.account.signer, ) { - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } } if (draftTag != null) { @@ -533,7 +540,6 @@ class ChatNewMessageViewModel : subject = TextFieldValue("") replyTo.value = null - expirationDays = null wantsInvoice = false wantsZapraiser = false @@ -742,5 +748,13 @@ class ChatNewMessageViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationManager(): LocationState = Amethyst.instance.locationManager } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index c10ac9f862..3a2f1f8599 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -97,6 +97,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -244,6 +246,10 @@ fun GroupDMScreenContent( ) } + if (postViewModel.wantsExpirationDate) { + ExpirationDatePicker(postViewModel) + } + if (postViewModel.wantsToAddGeoHash) { LocationAsHash(postViewModel) } @@ -458,6 +464,10 @@ private fun BottomRowActions( postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index db52197003..2615ac2ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send +import android.R.attr.maxLines import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,20 +34,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote @@ -134,10 +135,14 @@ fun PrivateMessageEditFieldRow( ) } - channelScreenModel.expirationDays?.let { + if (channelScreenModel.wantsExpirationDate) { Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.Center) { + val context = LocalContext.current Text( - stringResource(R.string.this_message_will_disappear_in_days, it), + stringRes( + R.string.this_message_will_disappear_in, + timeAheadNoDot(channelScreenModel.expirationDate, context), + ), fontSize = Font12SP, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, @@ -207,14 +212,6 @@ fun KeyboardLeadingIcon( onImageChosen = channelScreenModel::pickedMedia, ) - SelectFromFiles( - isUploading = channelScreenModel.isUploadingImage, - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier, - ) { - channelScreenModel.pickedMedia(it) - } - ToggleNip17Button(channelScreenModel, accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 9014fb1ac7..98b36ef0eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -50,6 +51,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder @@ -85,6 +87,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup @@ -92,6 +95,7 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -101,7 +105,8 @@ import kotlinx.coroutines.launch @Stable open class ChannelNewMessageViewModel : ViewModel(), - ILocationGrabber { + ILocationGrabber, + IExpiration { val draftTag = DraftTagState() init { @@ -149,6 +154,10 @@ open class ChannelNewMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -229,6 +238,10 @@ open class ChannelNewMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -388,6 +401,7 @@ open class ChannelNewMessageViewModel : val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null return if (channel is PublicChatChannel) { val replyingToEvent = replyTo.value?.toEventHint() @@ -401,6 +415,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -414,6 +429,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -426,6 +442,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -445,6 +462,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -457,6 +475,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -467,6 +486,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -482,6 +502,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -663,4 +684,12 @@ open class ChannelNewMessageViewModel : wantsToMarkAsSensitive = !wantsToMarkAsSensitive draftTag.newVersion() } + + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 5f781ace39..aa651348ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -230,6 +232,15 @@ private fun NewProductBody( } } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { Row( verticalAlignment = CenterVertically, @@ -388,6 +399,10 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index fc95787d25..be86ac1511 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -79,6 +81,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -89,6 +92,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.image import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -102,7 +106,8 @@ open class NewProductViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -160,6 +165,10 @@ open class NewProductViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -254,6 +263,10 @@ open class NewProductViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -333,6 +346,7 @@ open class NewProductViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val quotes = findNostrUris(tagger.message) @@ -353,6 +367,7 @@ open class NewProductViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -618,6 +633,14 @@ open class NewProductViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + fun updateTitle(it: TextFieldValue) { title = it draftTag.newVersion() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index ec7447f9b8..422394b5bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationControlle import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -144,7 +145,8 @@ open class ShortNotePostViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -242,7 +244,7 @@ open class ShortNotePostViewModel : // Expiration Date (NIP-40) var wantsExpirationDate by mutableStateOf(false) - var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) // GeoHash var wantsToAddGeoHash by mutableStateOf(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 50988c0654..a442d467cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -70,6 +70,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -218,6 +220,15 @@ fun PublicMessageScreenContent( ) } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { LocationAsHash(postViewModel) } @@ -350,6 +361,10 @@ private fun BottomRowActions( postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 7824f39746..d3ccd54210 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessag import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -86,6 +88,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits @@ -104,6 +107,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -116,7 +120,8 @@ class NewPublicMessageViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -171,6 +176,10 @@ class NewPublicMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -270,6 +279,10 @@ class NewPublicMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -368,6 +381,7 @@ class NewPublicMessageViewModel : val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null return PublicMessageEvent.build( to = toUsers.toList(), @@ -381,6 +395,7 @@ class NewPublicMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -663,5 +678,13 @@ class NewPublicMessageViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationManager(): LocationState = Amethyst.instance.locationManager } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 62e5657f88..27bc8972ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -91,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.DisplayDraft import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal +import com.vitorpamplona.amethyst.ui.note.Expiration import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture @@ -483,6 +484,8 @@ private fun FullBleedNoteCompose( CheckAndDisplayEditStatus(editState) + Expiration(baseNote) + TimeAgo(note = baseNote) MoreOptionsButton(baseNote, editState, accountViewModel, nav) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6aec4ff455..b6e772b0dd 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1538,6 +1538,7 @@ Crash Report found Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared Send it + This message will disappear in %1$s This message will disappear in %1$d days Select Signer