Adds expirations in all new post screens.

Adds expirations in the feed and chats.
This commit is contained in:
Vitor Pamplona
2026-03-13 18:05:59 -04:00
parent 42fdedf23b
commit c4c73a2bb7
18 changed files with 316 additions and 32 deletions
@@ -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<GenericLoadable<EditState>>) {
if (editState.value is GenericLoadable.Loaded) {
@@ -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) }
@@ -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
}
@@ -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<LocationState.LocationResult>? = 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<LocationState.LocationResult> {
if (location == null) {
location = locationManager().geohashStateFlow
@@ -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
}
@@ -204,6 +204,7 @@ fun NormalChatNote(
) {
IncognitoBadge(note)
ChatTimeAgo(note)
ChatExpiration(note)
RelayBadgesHorizontal(note, accountViewModel, nav = nav)
Spacer(modifier = DoubleHorzSpacer)
@@ -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,
)
}
}
@@ -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<LocationState.LocationResult>? = null
@@ -172,8 +179,6 @@ class ChatNewMessageViewModel :
var wantsZapraiser by mutableStateOf(false)
override var zapRaiserAmount = mutableStateOf<Long?>(null)
var expirationDays by mutableStateOf<Int?>(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<BaseDMGroupEvent>()
@@ -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<PrivateDmEvent>(),
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
}
@@ -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
}
@@ -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)
}
}
@@ -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<LocationState.LocationResult>? = 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<ChannelMessageEvent>()
@@ -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()
}
}
@@ -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
}
@@ -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<LocationState.LocationResult>? = 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()
@@ -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)
@@ -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
}
@@ -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<LocationState.LocationResult>? = 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
}
@@ -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)
+1
View File
@@ -1538,6 +1538,7 @@
<string name="crashreport_found">Crash Report found</string>
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared</string>
<string name="crashreport_found_send">Send it</string>
<string name="this_message_will_disappear_in">This message will disappear in %1$s</string>
<string name="this_message_will_disappear_in_days">This message will disappear in %1$d days</string>
<string name="select_signer">Select Signer</string>