Adds support for expirations in DMs
This commit is contained in:
@@ -144,7 +144,7 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.PublicChatChannel> {
|
||||
|
||||
+10
-3
@@ -138,6 +138,7 @@ fun routeToMessage(
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): Route =
|
||||
routeToMessage(
|
||||
@@ -145,6 +146,7 @@ fun routeToMessage(
|
||||
draftMessage,
|
||||
replyId,
|
||||
draftId,
|
||||
expiresDays,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
@@ -153,12 +155,14 @@ fun routeToMessage(
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) = routeToMessage(
|
||||
ChatroomKey(users),
|
||||
draftMessage,
|
||||
replyId,
|
||||
draftId,
|
||||
expiresDays,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
@@ -167,19 +171,21 @@ fun routeToMessage(
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): Route = routeToMessage(room, draftMessage, replyId, draftId, accountViewModel.account)
|
||||
): Route = routeToMessage(room, draftMessage, replyId, draftId, expiresDays, accountViewModel.account)
|
||||
|
||||
fun routeToMessage(
|
||||
room: ChatroomKey,
|
||||
draftMessage: String? = null,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
account: Account,
|
||||
): Route {
|
||||
account.chatroomList.getOrCreatePrivateChatroom(room)
|
||||
|
||||
return Route.Room(room, draftMessage, replyId, draftId)
|
||||
return Route.Room(room, draftMessage, replyId, draftId, expiresDays)
|
||||
}
|
||||
|
||||
fun routeToMessage(
|
||||
@@ -187,8 +193,9 @@ fun routeToMessage(
|
||||
draftMessage: String?,
|
||||
replyId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, accountViewModel)
|
||||
): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, expiresDays, accountViewModel)
|
||||
|
||||
fun routeFor(note: EphemeralChatChannel): Route = Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url)
|
||||
|
||||
|
||||
@@ -126,12 +126,14 @@ sealed class Route {
|
||||
val message: String? = null,
|
||||
val replyId: HexKey? = null,
|
||||
val draftId: HexKey? = null,
|
||||
val expiresDays: Int? = null,
|
||||
) : Route() {
|
||||
constructor(key: ChatroomKey, message: String? = null, replyId: HexKey? = null, draftId: HexKey? = null) : this(
|
||||
constructor(key: ChatroomKey, message: String? = null, replyId: HexKey? = null, draftId: HexKey? = null, expiresDays: Int? = null) : this(
|
||||
id = key.users.joinToString(","),
|
||||
message = message,
|
||||
replyId = replyId,
|
||||
draftId = draftId,
|
||||
expiresDays = expiresDays,
|
||||
)
|
||||
|
||||
fun toKey(): ChatroomKey = ChatroomKey(id.split(",").toSet())
|
||||
|
||||
+2
@@ -41,6 +41,7 @@ fun ChatroomScreen(
|
||||
draftMessage: String? = null,
|
||||
replyToNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -57,6 +58,7 @@ fun ChatroomScreen(
|
||||
draftMessage = draftMessage,
|
||||
replyToNote = replyToNote,
|
||||
editFromDraft = editFromDraft,
|
||||
expiresDays = expiresDays,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
+7
-1
@@ -52,6 +52,7 @@ fun ChatroomView(
|
||||
draftMessage: String?,
|
||||
replyToNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
expiresDays: Int? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -79,7 +80,7 @@ fun ChatroomView(
|
||||
}
|
||||
}
|
||||
if (editFromDraft != null) {
|
||||
LaunchedEffect(replyToNote, newPostModel, accountViewModel) {
|
||||
LaunchedEffect(editFromDraft, newPostModel, accountViewModel) {
|
||||
accountViewModel.checkGetOrCreateNote(editFromDraft) {
|
||||
if (it != null) {
|
||||
newPostModel.editFromDraft(it)
|
||||
@@ -87,6 +88,11 @@ fun ChatroomView(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expiresDays != null) {
|
||||
LaunchedEffect(expiresDays, newPostModel, accountViewModel) {
|
||||
newPostModel.loadExpiration(expiresDays)
|
||||
}
|
||||
}
|
||||
|
||||
if (room.users.size == 1) {
|
||||
// Activates NIP-17 if the user has DM relays
|
||||
|
||||
+18
-1
@@ -80,6 +80,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
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
|
||||
@@ -88,6 +89,7 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -168,6 +170,8 @@ 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)
|
||||
|
||||
@@ -267,6 +271,10 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun loadExpiration(expirationDays: Int) {
|
||||
this.expirationDays = expirationDays
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
|
||||
@@ -341,6 +349,8 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
urlPreviews.update(message)
|
||||
|
||||
expirationDays = draftEvent.expiration()?.let { (it / 86_400).toInt() }
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
requiresNIP17 = draftEvent is NIP17Group
|
||||
@@ -438,6 +448,8 @@ class ChatNewMessageViewModel :
|
||||
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>()
|
||||
|
||||
@@ -452,6 +464,7 @@ class ChatNewMessageViewModel :
|
||||
localZapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
expiration?.let { expiration(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
@@ -466,6 +479,7 @@ class ChatNewMessageViewModel :
|
||||
localZapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
expiration?.let { expiration(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
@@ -487,7 +501,9 @@ class ChatNewMessageViewModel :
|
||||
imetas = usedAttachments,
|
||||
replyingTo = replyTo.value?.toEId(),
|
||||
signer = accountViewModel.account.signer,
|
||||
)
|
||||
) {
|
||||
expiration?.let { expiration(it) }
|
||||
}
|
||||
|
||||
if (draftTag != null) {
|
||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)
|
||||
@@ -518,6 +534,7 @@ class ChatNewMessageViewModel :
|
||||
subject = TextFieldValue("")
|
||||
|
||||
replyTo.value = null
|
||||
expirationDays = null
|
||||
|
||||
wantsInvoice = false
|
||||
wantsZapraiser = false
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ fun NewGroupDMScreen(
|
||||
accountViewModel.runIOCatching {
|
||||
postViewModel.sendPostSync()
|
||||
postViewModel.room?.let {
|
||||
nav.nav(routeToMessage(it, null, null, null, accountViewModel))
|
||||
nav.nav(routeToMessage(it, null, null, null, null, accountViewModel))
|
||||
}
|
||||
}
|
||||
nav.popBack()
|
||||
|
||||
+16
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -32,6 +33,8 @@ 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.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -52,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.PostKeyboard
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -127,6 +131,18 @@ fun PrivateMessageEditFieldRow(
|
||||
)
|
||||
}
|
||||
|
||||
channelScreenModel.expirationDays?.let {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
stringResource(R.string.this_message_will_disappear_in_days, it),
|
||||
fontSize = Font12SP,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EditField(channelScreenModel, onSendNewMessage, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -110,6 +110,7 @@ fun MessagesTwoPane(
|
||||
draftMessage = it.message,
|
||||
replyToNote = it.replyId,
|
||||
editFromDraft = it.draftId,
|
||||
expiresDays = it.expiresDays,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1249,4 +1249,5 @@
|
||||
<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_days">This message will disappear in %1$d days</string>
|
||||
</resources>
|
||||
|
||||
@@ -29,9 +29,11 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
import java.time.temporal.TemporalAdjusters.next
|
||||
|
||||
class NIP17Factory {
|
||||
data class Result(
|
||||
@@ -43,8 +45,17 @@ class NIP17Factory {
|
||||
event: Event,
|
||||
to: Set<HexKey>,
|
||||
signer: NostrSigner,
|
||||
): List<GiftWrapEvent> =
|
||||
mapNotNullAsync(
|
||||
): List<GiftWrapEvent> {
|
||||
val innerExpDelta =
|
||||
event.expiration()?.let {
|
||||
if (it > event.createdAt) {
|
||||
it - event.createdAt
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return mapNotNullAsync(
|
||||
to.toList(),
|
||||
) { next ->
|
||||
GiftWrapEvent.create(
|
||||
@@ -52,11 +63,14 @@ class NIP17Factory {
|
||||
SealedRumorEvent.create(
|
||||
event = event,
|
||||
encryptTo = next,
|
||||
expirationDelta = innerExpDelta,
|
||||
signer = signer,
|
||||
),
|
||||
recipientPubKey = next,
|
||||
expirationDelta = innerExpDelta,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createMessageNIP17(
|
||||
template: EventTemplate<ChatMessageEvent>,
|
||||
|
||||
+13
-2
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HostStub
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
|
||||
@@ -99,23 +100,33 @@ class SealedRumorEvent(
|
||||
event: Event,
|
||||
encryptTo: HexKey,
|
||||
signer: NostrSigner,
|
||||
expirationDelta: Long? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): SealedRumorEvent {
|
||||
val rumor = Rumor.create(event)
|
||||
return create(rumor, encryptTo, signer, createdAt)
|
||||
return create(rumor, encryptTo, signer, expirationDelta, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
rumor: Rumor,
|
||||
encryptTo: HexKey,
|
||||
signer: NostrSigner,
|
||||
expirationDelta: Long? = null,
|
||||
createdAt: Long = TimeUtils.randomWithTwoDays(),
|
||||
): SealedRumorEvent {
|
||||
val msg = Rumor.toJson(rumor)
|
||||
|
||||
val tags =
|
||||
expirationDelta?.let {
|
||||
// minimum expiration is two days in the future due to the random created at
|
||||
// this will make sure the even arrives and is not deleted because of the 2 days.
|
||||
arrayOf(ExpirationTag.assemble(createdAt + it + TimeUtils.twoDays()))
|
||||
} ?: emptyArray()
|
||||
|
||||
return signer.sign(
|
||||
createdAt = createdAt,
|
||||
kind = KIND,
|
||||
tags = emptyArray(),
|
||||
tags = tags,
|
||||
content = signer.nip44Encrypt(msg, encryptTo),
|
||||
)
|
||||
}
|
||||
|
||||
+17
-1
@@ -28,7 +28,9 @@ import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.HostStub
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -103,13 +105,27 @@ class GiftWrapEvent(
|
||||
suspend fun create(
|
||||
event: Event,
|
||||
recipientPubKey: HexKey,
|
||||
expirationDelta: Long? = null,
|
||||
createdAt: Long = TimeUtils.randomWithTwoDays(),
|
||||
): GiftWrapEvent {
|
||||
val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key
|
||||
|
||||
val tags =
|
||||
expirationDelta?.let {
|
||||
// minimum expiration is two days in the future due to the random created at
|
||||
// this will make sure the even arrives and is not deleted because of the 2 days.
|
||||
arrayOf(
|
||||
PTag.assemble(recipientPubKey, null),
|
||||
ExpirationTag.assemble(createdAt + it + TimeUtils.twoDays()),
|
||||
)
|
||||
} ?: arrayOf(
|
||||
PTag.assemble(recipientPubKey, null),
|
||||
)
|
||||
|
||||
return signer.sign(
|
||||
createdAt = createdAt,
|
||||
kind = KIND,
|
||||
tags = arrayOf(arrayOf("p", recipientPubKey)),
|
||||
tags = tags,
|
||||
content = signer.nip44Encrypt(event.toJson(), recipientPubKey),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user