Merge pull request #749 from greenart7c3/main

save a draft while you are typing the post
This commit is contained in:
Vitor Pamplona
2024-03-26 15:24:59 -04:00
committed by GitHub
41 changed files with 1166 additions and 243 deletions

View File

@@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.Contact import com.vitorpamplona.quartz.events.Contact
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.EmojiUrl
@@ -845,7 +846,14 @@ class Account(
} }
suspend fun delete(note: Note) { suspend fun delete(note: Note) {
return delete(listOf(note)) if (note.isDraft()) {
note.event?.let {
val drafts = LocalCache.getDrafts(it.id())
return delete(drafts)
}
} else {
return delete(listOf(note))
}
} }
suspend fun delete(notes: List<Note>) { suspend fun delete(notes: List<Note>) {
@@ -897,10 +905,17 @@ class Account(
fun broadcast(note: Note) { fun broadcast(note: Note) {
note.event?.let { note.event?.let {
if (it is WrappedEvent && it.host != null) { if (note.isDraft()) {
it.host?.let { hostEvent -> Client.send(hostEvent) } val drafts = LocalCache.getDrafts(it.id())
drafts.forEach { draftNote ->
broadcast(draftNote)
}
} else { } else {
Client.send(it) if (it is WrappedEvent && it.host != null) {
it.host?.let { hostEvent -> Client.send(hostEvent) }
} else {
Client.send(it)
}
} }
} }
} }
@@ -929,6 +944,7 @@ class Account(
fun timestamp(note: Note) { fun timestamp(note: Note) {
if (!isWriteable()) return if (!isWriteable()) return
if (note.isDraft()) return
val id = note.event?.id() ?: note.idHex val id = note.event?.id() ?: note.idHex
@@ -1318,6 +1334,7 @@ class Account(
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<Event>? = null, nip94attachments: List<Event>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1345,14 +1362,24 @@ class Account(
geohash = geohash, geohash = geohash,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
signer = signer, signer = signer,
isDraft = draftTag != null,
) { ) {
Client.send(it, relayList = relayList) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
}
} }
} }
} }
@@ -1373,6 +1400,7 @@ class Account(
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1396,20 +1424,30 @@ class Account(
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
forkedFrom = forkedFrom, forkedFrom = forkedFrom,
signer = signer, signer = signer,
isDraft = draftTag != null,
) { ) {
Client.send(it, relayList = relayList) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
// broadcast replied notes LocalCache.justConsume(draftEvent, null)
replyingTo?.let { LocalCache.justConsume(it, null)
LocalCache.getNoteIfExists(replyingTo)?.event?.let { LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
Client.send(it, relayList = relayList)
} }
} } else {
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } Client.send(it, relayList = relayList)
addresses?.forEach { LocalCache.justConsume(it, null)
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) // broadcast replied notes
replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList)
}
}
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
} }
} }
} }
@@ -1430,6 +1468,7 @@ class Account(
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1453,20 +1492,30 @@ class Account(
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
forkedFrom = forkedFrom, forkedFrom = forkedFrom,
signer = signer, signer = signer,
isDraft = draftTag != null,
) { ) {
Client.send(it, relayList = relayList) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
// broadcast replied notes LocalCache.justConsume(draftEvent, null)
replyingTo?.let { LocalCache.justConsume(it, null)
LocalCache.getNoteIfExists(replyingTo)?.event?.let { LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
Client.send(it, relayList = relayList)
} }
} } else {
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } Client.send(it, relayList = relayList)
addresses?.forEach { LocalCache.justConsume(it, null)
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) // broadcast replied notes
replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList)
}
}
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
} }
} }
} }
@@ -1510,6 +1559,7 @@ class Account(
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1533,15 +1583,25 @@ class Account(
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
geohash = geohash, geohash = geohash,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
isDraft = draftTag != null,
) { ) {
Client.send(it, relayList = relayList) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
// Rebroadcast replies and tags to the current relay set // Rebroadcast replies and tags to the current relay set
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
}
} }
} }
} }
@@ -1557,6 +1617,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1574,9 +1635,19 @@ class Account(
geohash = geohash, geohash = geohash,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
signer = signer, signer = signer,
isDraft = draftTag != null,
) { ) {
Client.send(it) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.justConsume(it, null)
}
} }
} }
@@ -1590,6 +1661,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1608,9 +1680,19 @@ class Account(
geohash = geohash, geohash = geohash,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
signer = signer, signer = signer,
isDraft = draftTag != null,
) { ) {
Client.send(it) if (draftTag != null) {
LocalCache.justConsume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.justConsume(it, null)
}
} }
} }
@@ -1624,6 +1706,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
sendPrivateMessage( sendPrivateMessage(
message, message,
@@ -1635,6 +1718,7 @@ class Account(
zapRaiserAmount, zapRaiserAmount,
geohash, geohash,
nip94attachments, nip94attachments,
draftTag,
) )
} }
@@ -1648,6 +1732,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1667,9 +1752,19 @@ class Account(
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
signer = signer, signer = signer,
advertiseNip18 = false, advertiseNip18 = false,
isDraft = draftTag != null,
) { ) {
Client.send(it) if (draftTag != null) {
LocalCache.consume(it, null) DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.consume(it, null)
}
} }
} }
@@ -1684,6 +1779,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String? = null,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -1701,9 +1797,19 @@ class Account(
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
geohash = geohash, geohash = geohash,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
draftTag = draftTag,
signer = signer, signer = signer,
) { ) {
broadcastPrivately(it) if (draftTag != null) {
DraftEvent.create(draftTag, it.msg, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it.msg, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.msg.id())
}
} else {
broadcastPrivately(it)
}
} }
} }
@@ -1851,6 +1957,7 @@ class Account(
isPrivate: Boolean, isPrivate: Boolean,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
if (note.isDraft()) return
if (note is AddressableNote) { if (note is AddressableNote) {
BookmarkListEvent.addReplaceable( BookmarkListEvent.addReplaceable(
@@ -2218,6 +2325,7 @@ class Account(
fun cachedDecryptContent(note: Note): String? { fun cachedDecryptContent(note: Note): String? {
val event = note.event val event = note.event
return if (event is PrivateDmEvent && isWriteable()) { return if (event is PrivateDmEvent && isWriteable()) {
event.cachedContentFor(signer) event.cachedContentFor(signer)
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) { } else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2024 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.model
data class Drafts(val mainId: String, val eventId: String)

View File

@@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.events.CommunityListEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
@@ -128,7 +129,7 @@ object LocalCache {
val users = LargeCache<HexKey, User>() val users = LargeCache<HexKey, User>()
val notes = LargeCache<HexKey, Note>() val notes = LargeCache<HexKey, Note>()
val addressables = LargeCache<String, AddressableNote>() val addressables = LargeCache<String, AddressableNote>()
val drafts = ConcurrentHashMap<String, MutableList<Drafts>>()
val channels = LargeCache<HexKey, Channel>() val channels = LargeCache<HexKey, Channel>()
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10) val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
@@ -141,6 +142,34 @@ object LocalCache {
return null return null
} }
fun draftNotes(draftTag: String): List<Note> {
return drafts[draftTag]?.mapNotNull {
getNoteIfExists(it.mainId)
} ?: listOf()
}
fun getDrafts(eventId: String): List<Note> {
return drafts.filter {
it.value.any { it.eventId == eventId }
}.values.map {
it.mapNotNull {
checkGetOrCreateNote(it.mainId)
}
}.flatten()
}
fun addDraft(
key: String,
mainId: String,
draftId: String,
) {
val data = drafts[key] ?: mutableListOf()
if (data.none { it.mainId == mainId }) {
data.add(Drafts(mainId, draftId))
drafts[key] = data
}
}
fun getOrCreateUser(key: HexKey): User { fun getOrCreateUser(key: HexKey): User {
// checkNotInMainThread() // checkNotInMainThread()
require(isValidHex(key = key)) { "$key is not a valid hex" } require(isValidHex(key = key)) { "$key is not a valid hex" }
@@ -2013,6 +2042,13 @@ object LocalCache {
} }
} }
private fun consume(
event: DraftEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
fun justConsume( fun justConsume(
event: Event, event: Event,
relay: Relay?, relay: Relay?,
@@ -2050,6 +2086,7 @@ object LocalCache {
} }
is ContactListEvent -> consume(event) is ContactListEvent -> consume(event)
is DeletionEvent -> consume(event) is DeletionEvent -> consume(event)
is DraftEvent -> consume(event, relay)
is EmojiPackEvent -> consume(event, relay) is EmojiPackEvent -> consume(event, relay)
is EmojiPackSelectionEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay)
is SealedGossipEvent -> consume(event, relay) is SealedGossipEvent -> consume(event, relay)

View File

@@ -184,6 +184,13 @@ open class Note(val idHex: String) {
open fun createdAt() = event?.createdAt() open fun createdAt() = event?.createdAt()
fun isDraft(): Boolean {
event?.let {
return it.sig().isBlank()
}
return false
}
fun loadEvent( fun loadEvent(
event: Event, event: Event,
author: User, author: User,

View File

@@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
@@ -229,6 +230,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
) )
} }
fun createDraftsFilter() =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(DraftEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
),
)
fun createGiftWrapsToMeFilter() = fun createGiftWrapsToMeFilter() =
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
@@ -262,22 +273,46 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
checkNotInMainThread() checkNotInMainThread()
if (LocalCache.justVerify(event)) { if (LocalCache.justVerify(event)) {
if (event is GiftWrapEvent) { when (event) {
// Avoid decrypting over and over again if the event already exist. is DraftEvent -> {
val note = LocalCache.getNoteIfExists(event.id) // Avoid decrypting over and over again if the event already exist.
if (note != null && relay.brief in note.relays) return
event.cachedGift(account.signer) { this.consume(it, relay) } val note = LocalCache.getNoteIfExists(event.id)
} if (note != null && relay.brief in note.relays) return
if (event is SealedGossipEvent) { LocalCache.justConsume(event, relay)
// Avoid decrypting over and over again if the event already exist. event.plainContent(account.signer) {
val note = LocalCache.getNoteIfExists(event.id) val tag =
if (note != null && relay.brief in note.relays) return event.tags().filter { it.size > 1 && it[0] == "d" }.map {
it[1]
}.firstOrNull()
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) } LocalCache.justConsume(it, relay)
} else { tag?.let { lTag ->
LocalCache.justConsume(event, relay) LocalCache.addDraft(lTag, event.id(), it.id())
}
}
}
is GiftWrapEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
event.cachedGift(account.signer) { this.consume(it, relay) }
}
is SealedGossipEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
}
else -> {
LocalCache.justConsume(event, relay)
}
} }
} }
} }
@@ -328,6 +363,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountSettingsFilter(), createAccountSettingsFilter(),
createAccountLastPostsListFilter(), createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(), createOtherAccountsBaseFilter(),
createDraftsFilter(),
) )
.ifEmpty { null } .ifEmpty { null }
} else { } else {

View File

@@ -34,8 +34,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun NewPollOption( fun NewPollOption(
@@ -45,7 +48,12 @@ fun NewPollOption(
Row { Row {
val deleteIcon: @Composable (() -> Unit) = { val deleteIcon: @Composable (() -> Unit) = {
IconButton( IconButton(
onClick = { pollViewModel.pollOptions.remove(optionIndex) }, onClick = {
pollViewModel.pollOptions.remove(optionIndex)
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
},
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
@@ -57,7 +65,12 @@ fun NewPollOption(
OutlinedTextField( OutlinedTextField(
modifier = Modifier.weight(1F), modifier = Modifier.weight(1F),
value = pollViewModel.pollOptions[optionIndex] ?: "", value = pollViewModel.pollOptions[optionIndex] ?: "",
onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, onValueChange = {
pollViewModel.pollOptions[optionIndex] = it
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
},
label = { label = {
Text( Text(
text = stringResource(R.string.poll_option_index).format(optionIndex + 1), text = stringResource(R.string.poll_option_index).format(optionIndex + 1),

View File

@@ -119,6 +119,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -171,13 +172,18 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.Math.round import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Composable @Composable
fun NewPostView( fun NewPostView(
onClose: () -> Unit, onClose: () -> Unit,
@@ -185,6 +191,7 @@ fun NewPostView(
quote: Note? = null, quote: Note? = null,
fork: Note? = null, fork: Note? = null,
version: Note? = null, version: Note? = null,
draft: Note? = null,
enableMessageInterface: Boolean = false, enableMessageInterface: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@@ -200,9 +207,17 @@ fun NewPostView(
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version)
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
postViewModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
postViewModel.sendPost(relayList = relayList, localDraft = postViewModel.draftTag)
}
}
launch(Dispatchers.IO) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft)
postViewModel.imageUploadingError.collect { error -> postViewModel.imageUploadingError.collect { error ->
withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() }
} }
@@ -582,6 +597,9 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
MarkAsSensitive(postViewModel) { MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
} }
AddGeoHash(postViewModel) { AddGeoHash(postViewModel) {
@@ -827,7 +845,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField( MyTextField(
value = postViewModel.title, value = postViewModel.title,
onValueChange = { postViewModel.title = it }, onValueChange = {
postViewModel.title = it
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {
Text( Text(
@@ -870,6 +893,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
postViewModel.price = it postViewModel.price = it
} }
} }
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
placeholder = { placeholder = {
Text( Text(
@@ -934,7 +960,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
TextSpinner( TextSpinner(
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
options = conditionOptions, options = conditionOptions,
onSelect = { postViewModel.condition = conditionTypes[it].first }, onSelect = {
postViewModel.condition = conditionTypes[it].first
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = modifier =
Modifier Modifier
.weight(1f) .weight(1f)
@@ -998,7 +1029,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second
?: "", ?: "",
options = categoryOptions, options = categoryOptions,
onSelect = { postViewModel.category = TextFieldValue(categoryTypes[it].second) }, onSelect = {
postViewModel.category = TextFieldValue(categoryTypes[it].second)
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = modifier =
Modifier Modifier
.weight(1f) .weight(1f)
@@ -1033,7 +1069,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField( MyTextField(
value = postViewModel.locationText, value = postViewModel.locationText,
onValueChange = { postViewModel.locationText = it }, onValueChange = {
postViewModel.locationText = it
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {
Text( Text(

View File

@@ -69,10 +69,12 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID
enum class UserSuggestionAnchor { enum class UserSuggestionAnchor {
MAIN_MESSAGE, MAIN_MESSAGE,
@@ -82,6 +84,7 @@ enum class UserSuggestionAnchor {
@Stable @Stable
open class NewPostViewModel() : ViewModel() { open class NewPostViewModel() : ViewModel() {
var draftTag: String = UUID.randomUUID().toString()
var accountViewModel: AccountViewModel? = null var accountViewModel: AccountViewModel? = null
var account: Account? = null var account: Account? = null
var requiresNIP24: Boolean = false var requiresNIP24: Boolean = false
@@ -164,6 +167,8 @@ open class NewPostViewModel() : ViewModel() {
// NIP24 Wrapped DMs / Group messages // NIP24 Wrapped DMs / Group messages
var nip24 by mutableStateOf(false) var nip24 by mutableStateOf(false)
val draftTextChanges = Channel<String>(Channel.CONFLATED)
fun lnAddress(): String? { fun lnAddress(): String? {
return account?.userProfile()?.info?.lnAddress() return account?.userProfile()?.info?.lnAddress()
} }
@@ -182,127 +187,236 @@ open class NewPostViewModel() : ViewModel() {
quote: Note?, quote: Note?,
fork: Note?, fork: Note?,
version: Note?, version: Note?,
draft: Note?,
) { ) {
this.accountViewModel = accountViewModel this.accountViewModel = accountViewModel
this.account = accountViewModel.account this.account = accountViewModel.account
originalNote = replyingTo if (draft != null) {
replyingTo?.let { replyNote -> loadFromDraft(draft, accountViewModel)
if (replyNote.event is BaseTextNoteEvent) { } else {
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote) originalNote = replyingTo
} else { replyingTo?.let { replyNote ->
this.eTags = listOf(replyNote) if (replyNote.event is BaseTextNoteEvent) {
} this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
} else {
this.eTags = listOf(replyNote)
}
if (replyNote.event !is CommunityDefinitionEvent) { if (replyNote.event !is CommunityDefinitionEvent) {
replyNote.author?.let { replyUser -> replyNote.author?.let { replyUser ->
val currentMentions = val currentMentions =
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) } (replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
?: emptyList() ?: emptyList()
if (currentMentions.contains(replyUser)) { if (currentMentions.contains(replyUser)) {
this.pTags = currentMentions this.pTags = currentMentions
} else { } else {
this.pTags = currentMentions.plus(replyUser) this.pTags = currentMentions.plus(replyUser)
}
} }
} }
} }
} ?: run {
?: run { eTags = null
eTags = null pTags = null
pTags = null }
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
quote?.let {
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
urlPreview = findUrlInMessage()
it.author?.let { quotedUser ->
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
forwardZapTo.addItem(quotedUser)
}
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
forwardZapTo.addItem(accountViewModel.userProfile())
}
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.9f)
}
}
} }
fork?.let {
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
urlPreview = findUrlInMessage()
it.event?.isSensitive()?.let {
if (it) wantsToMarkAsSensitive = true
}
it.event?.zapraiserAmount()?.let {
zapRaiserAmount = it
}
it.event?.zapSplitSetup()?.let {
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
it.forEach {
if (!it.isLnAddress) {
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
}
}
}
// Only adds if it is not already set up.
if (forwardZapTo.items.isEmpty()) {
it.author?.let { forkedAuthor ->
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.8f)
}
}
}
it.author?.let {
if (this.pTags == null) {
this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) {
this.pTags = listOf(it) + (this.pTags ?: emptyList())
}
}
forkedFromNote = it
} ?: run {
forkedFromNote = null
}
if (!forwardZapTo.items.isEmpty()) {
wantsForwardZapTo = true
}
}
}
private fun loadFromDraft(
draft: Note,
accountViewModel: AccountViewModel,
) {
Log.d("draft", draft.event!!.toJson())
draftTag = LocalCache.drafts.filter {
it.value.any { it.eventId == draft.event?.id() }
}.keys.firstOrNull() ?: draftTag
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null contentToAddUrl = null
wantsForwardZapTo = false val localfowardZapTo = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zap" } ?: listOf()
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = Split() forwardZapTo = Split()
localfowardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.addItem(user, value)
}
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
quote?.let { wantsToMarkAsSensitive = draft.event?.tags()?.any { it.size > 1 && it[0] == "content-warning" } ?: false
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") wantsToAddGeoHash = draft.event?.tags()?.any { it.size > 1 && it[0] == "g" } ?: false
urlPreview = findUrlInMessage() val zapraiser = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zapraiser" } ?: listOf()
wantsZapraiser = zapraiser.isNotEmpty()
it.author?.let { quotedUser -> zapRaiserAmount = null
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { if (wantsZapraiser) {
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { zapRaiserAmount = zapraiser.first()[1].toLongOrNull() ?: 0
forwardZapTo.addItem(quotedUser)
}
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
forwardZapTo.addItem(accountViewModel.userProfile())
}
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.9f)
}
}
} }
fork?.let { eTags =
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "") draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }?.mapNotNull {
urlPreview = findUrlInMessage() val note = LocalCache.checkGetOrCreateNote(it[1])
note
it.event?.isSensitive()?.let {
if (it) wantsToMarkAsSensitive = true
} }
it.event?.zapraiserAmount()?.let { pTags =
zapRaiserAmount = it draft.event?.tags()?.filter { it.size > 1 && it[0] == "p" }?.map {
LocalCache.getOrCreateUser(it[1])
} }
it.event?.zapSplitSetup()?.let { draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "fork" }?.forEach {
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight } val note = LocalCache.checkGetOrCreateNote(it[1])
forkedFromNote = note
it.forEach {
if (!it.isLnAddress) {
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
}
}
}
// Only adds if it is not already set up.
if (forwardZapTo.items.isEmpty()) {
it.author?.let { forkedAuthor ->
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.8f)
}
}
}
it.author?.let {
if (this.pTags == null) {
this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) {
this.pTags = listOf(it) + (this.pTags ?: emptyList())
}
}
forkedFromNote = it
} ?: run {
forkedFromNote = null
} }
if (!forwardZapTo.items.isEmpty()) { originalNote =
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }?.map {
LocalCache.checkGetOrCreateNote(it[1])
}?.firstOrNull()
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
if (forwardZapTo.items.isNotEmpty()) {
wantsForwardZapTo = true wantsForwardZapTo = true
} }
val polls = draft.event?.tags()?.filter { it.size > 1 && it[0] == "poll_option" } ?: emptyList()
wantsPoll = polls.isNotEmpty()
polls.forEach {
pollOptions[it[1].toInt()] = it[2]
}
val minMax = draft.event?.tags()?.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") } ?: listOf()
minMax.forEach {
if (it[0] == "value_maximum") {
valueMaximum = it[1].toInt()
} else if (it[0] == "value_minimum") {
valueMinimum = it[1].toInt()
}
}
wantsProduct = draft.event?.kind() == 30402
title = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "title" }?.map { it[1] }?.firstOrNull() ?: "")
price = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "price" }?.map { it[1] }?.firstOrNull() ?: "")
category = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }?.firstOrNull() ?: "")
locationText = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "location" }?.map { it[1] }?.firstOrNull() ?: "")
condition = ClassifiedsEvent.CONDITION.entries.firstOrNull {
it.value == draft.event?.tags()?.filter { it.size > 1 && it[0] == "condition" }?.map { it[1] }?.firstOrNull()
} ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW
message =
if (draft.event is PrivateDmEvent) {
val event = draft.event as PrivateDmEvent
TextFieldValue(event.cachedContentFor(accountViewModel.account.signer) ?: "")
} else {
TextFieldValue(draft.event?.content() ?: "")
}
nip24 = draft.event is ChatMessageEvent
urlPreview = findUrlInMessage()
} }
fun sendPost(relayList: List<Relay>? = null) { fun sendPost(
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } relayList: List<Relay>? = null,
localDraft: String? = null,
) {
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList, localDraft) }
} }
suspend fun innerSendPost(relayList: List<Relay>? = null) { private suspend fun innerSendPost(
relayList: List<Relay>? = null,
localDraft: String?,
) {
if (accountViewModel == null) { if (accountViewModel == null) {
cancel() cancel()
return return
@@ -363,6 +477,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else { } else {
account?.sendChannelMessage( account?.sendChannelMessage(
@@ -375,6 +490,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} }
} else if (originalNote?.event is PrivateDmEvent) { } else if (originalNote?.event is PrivateDmEvent) {
@@ -388,6 +504,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else if (originalNote?.event is ChatMessageEvent) { } else if (originalNote?.event is ChatMessageEvent) {
val receivers = val receivers =
@@ -423,6 +540,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else { } else {
account?.sendPrivateMessage( account?.sendPrivateMessage(
@@ -435,6 +553,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} }
} else if (originalNote?.event is GitIssueEvent) { } else if (originalNote?.event is GitIssueEvent) {
@@ -475,6 +594,7 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else { } else {
if (wantsPoll) { if (wantsPoll) {
@@ -493,6 +613,7 @@ open class NewPostViewModel() : ViewModel() {
relayList, relayList,
geoHash, geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else if (wantsProduct) { } else if (wantsProduct) {
account?.sendClassifieds( account?.sendClassifieds(
@@ -511,6 +632,7 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else { } else {
// adds markers // adds markers
@@ -547,11 +669,13 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} }
} }
if (localDraft == null) {
cancel() cancel()
}
} }
fun upload( fun upload(
@@ -635,6 +759,7 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = null urlPreview = null
isUploadingImage = false isUploadingImage = false
pTags = null pTags = null
eTags = null
wantsDirectMessage = false wantsDirectMessage = false
@@ -663,6 +788,11 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
userSuggestionAnchor = null userSuggestionAnchor = null
userSuggestionsMainMessage = null userSuggestionsMainMessage = null
originalNote = null
viewModelScope.launch(Dispatchers.IO) {
accountViewModel?.deleteDraft(draftTag)
}
NostrSearchEventOrUserDataSource.clear() NostrSearchEventOrUserDataSource.clear()
} }
@@ -679,6 +809,10 @@ open class NewPostViewModel() : ViewModel() {
pTags = pTags?.filter { it != userToRemove } pTags = pTags?.filter { it != userToRemove }
} }
open suspend fun saveDraft() {
draftTextChanges.send("")
}
open fun updateMessage(it: TextFieldValue) { open fun updateMessage(it: TextFieldValue) {
message = it message = it
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
@@ -701,6 +835,10 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
open fun updateToUsers(it: TextFieldValue) { open fun updateToUsers(it: TextFieldValue) {
@@ -724,10 +862,16 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
open fun updateSubject(it: TextFieldValue) { open fun updateSubject(it: TextFieldValue) {
subject = it subject = it
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
open fun updateZapForwardTo(it: TextFieldValue) { open fun updateZapForwardTo(it: TextFieldValue) {
@@ -754,6 +898,9 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
open fun autocompleteWithUser(item: User) { open fun autocompleteWithUser(item: User) {
@@ -799,6 +946,10 @@ open class NewPostViewModel() : ViewModel() {
userSuggestionsMainMessage = null userSuggestionsMainMessage = null
userSuggestions = emptyList() userSuggestions = emptyList()
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> { private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
@@ -869,6 +1020,9 @@ open class NewPostViewModel() : ViewModel() {
message = message.insertUrlAtCursor(imageUrl) message = message.insertUrlAtCursor(imageUrl)
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
}, },
onError = { onError = {
@@ -913,6 +1067,9 @@ open class NewPostViewModel() : ViewModel() {
} }
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
}, },
onError = { onError = {
@@ -933,6 +1090,7 @@ open class NewPostViewModel() : ViewModel() {
locUtil?.let { locUtil?.let {
location = location =
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() } it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
viewModelScope.launch(Dispatchers.IO) { saveDraft() }
} }
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() } viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
} }
@@ -957,6 +1115,11 @@ open class NewPostViewModel() : ViewModel() {
} else { } else {
nip24 = !nip24 nip24 = !nip24
} }
if (message.text.isNotBlank()) {
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
} }
fun updateMinZapAmountForPoll(textMin: String) { fun updateMinZapAmountForPoll(textMin: String) {
@@ -976,6 +1139,9 @@ open class NewPostViewModel() : ViewModel() {
} }
checkMinMax() checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
fun updateMaxZapAmountForPoll(textMax: String) { fun updateMaxZapAmountForPoll(textMax: String) {
@@ -995,6 +1161,9 @@ open class NewPostViewModel() : ViewModel() {
} }
checkMinMax() checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
} }
fun checkMinMax() { fun checkMinMax() {

View File

@@ -39,6 +39,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning import com.vitorpamplona.amethyst.commons.hashtags.Lightning
@@ -46,6 +47,8 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun ZapRaiserRequest( fun ZapRaiserRequest(
@@ -98,6 +101,9 @@ fun ZapRaiserRequest(
} else { } else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull() newPostViewModel.zapRaiserAmount = it.toLongOrNull()
} }
newPostViewModel.viewModelScope.launch(Dispatchers.IO) {
newPostViewModel.saveDraft()
}
} }
}, },
placeholder = { placeholder = {

View File

@@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.ClickableText
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -159,7 +160,10 @@ fun DrawerContent(
) )
ListContent( ListContent(
modifier = Modifier.fillMaxWidth().weight(1f), modifier =
Modifier
.fillMaxWidth()
.weight(1f),
drawerState, drawerState,
openSheet, openSheet,
accountViewModel, accountViewModel,
@@ -231,7 +235,8 @@ fun ProfileContentTemplate(
model = profilePicture, model = profilePicture,
contentDescription = stringResource(id = R.string.profile_image), contentDescription = stringResource(id = R.string.profile_image),
modifier = modifier =
Modifier.width(100.dp) Modifier
.width(100.dp)
.height(100.dp) .height(100.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape) .border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
@@ -244,7 +249,10 @@ fun ProfileContentTemplate(
CreateTextWithEmoji( CreateTextWithEmoji(
text = bestDisplayName, text = bestDisplayName,
tags = tags, tags = tags,
modifier = Modifier.padding(top = 7.dp).clickable(onClick = onClick), modifier =
Modifier
.padding(top = 7.dp)
.clickable(onClick = onClick),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp, fontSize = 18.sp,
maxLines = 1, maxLines = 1,
@@ -454,8 +462,17 @@ fun ListContent(
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) } val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
val context = LocalContext.current val context = LocalContext.current
var draftText by remember {
mutableStateOf<String?>(null)
}
var wantsToPost by remember { mutableStateOf(false) }
Column( Column(
modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState()), modifier =
modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) { ) {
NavigationRow( NavigationRow(
title = stringResource(R.string.profile), title = stringResource(R.string.profile),
@@ -571,6 +588,18 @@ fun ListContent(
) )
} }
if (wantsToPost) {
NewPostView(
{
wantsToPost = false
draftText = null
coroutineScope.launch { drawerState.close() }
},
accountViewModel = accountViewModel,
nav = nav,
)
}
if (disconnectTorDialog) { if (disconnectTorDialog) {
AlertDialog( AlertDialog(
title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) }, title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) },
@@ -662,7 +691,8 @@ fun IconRow(
) { ) {
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier
.fillMaxWidth()
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
@@ -693,10 +723,16 @@ fun IconRowRelays(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().clickable { onClick() }, modifier =
Modifier
.fillMaxWidth()
.clickable { onClick() },
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp), modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 25.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
@@ -737,7 +773,10 @@ fun BottomContent(
thickness = DividerThickness, thickness = DividerThickness,
) )
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
ClickableText( ClickableText(

View File

@@ -147,7 +147,7 @@ fun NormalChannelCard(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup ->
CheckNewAndRenderChannelCard( CheckNewAndRenderChannelCard(
baseNote, baseNote,
routeForLastRead, routeForLastRead,

View File

@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
@@ -102,6 +103,7 @@ fun ChatroomMessageCompose(
innerQuote: Boolean = false, innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@@ -120,6 +122,7 @@ fun ChatroomMessageCompose(
canPreview, canPreview,
parentBackgroundColor, parentBackgroundColor,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
onWantsToReply, onWantsToReply,
) )
@@ -136,6 +139,7 @@ fun NormalChatNote(
canPreview: Boolean = true, canPreview: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@@ -255,6 +259,7 @@ fun NormalChatNote(
availableBubbleSize, availableBubbleSize,
showDetails, showDetails,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
) )
} }
@@ -265,6 +270,7 @@ fun NormalChatNote(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false }, onDismiss = { popupExpanded = false },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
) )
} }
} }
@@ -282,6 +288,7 @@ private fun RenderBubble(
availableBubbleSize: MutableState<Int>, availableBubbleSize: MutableState<Int>,
showDetails: State<Boolean>, showDetails: State<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val bubbleSize = remember { mutableIntStateOf(0) } val bubbleSize = remember { mutableIntStateOf(0) }
@@ -311,6 +318,7 @@ private fun RenderBubble(
canPreview, canPreview,
showDetails, showDetails,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
) )
} }
@@ -329,6 +337,7 @@ private fun MessageBubbleLines(
canPreview: Boolean, canPreview: Boolean,
showDetails: State<Boolean>, showDetails: State<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
if (drawAuthorInfo) { if (drawAuthorInfo) {
@@ -345,6 +354,7 @@ private fun MessageBubbleLines(
innerQuote = innerQuote, innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor, backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )
@@ -363,6 +373,9 @@ private fun MessageBubbleLines(
bubbleSize = bubbleSize, bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize, availableBubbleSize = availableBubbleSize,
firstColumn = { firstColumn = {
if (baseNote.isDraft()) {
DisplayDraftChat()
}
IncognitoBadge(baseNote) IncognitoBadge(baseNote)
ChatTimeAgo(baseNote) ChatTimeAgo(baseNote)
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav) RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
@@ -394,11 +407,12 @@ private fun RenderReplyRow(
innerQuote: Boolean, innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
if (!innerQuote && note.replyTo?.lastOrNull() != null) { if (!innerQuote && note.replyTo?.lastOrNull() != null) {
RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply) RenderReply(note, backgroundBubbleColor, accountViewModel, newPostViewModel, nav, onWantsToReply)
} }
} }
@@ -407,6 +421,7 @@ private fun RenderReply(
note: Note, note: Note,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@@ -425,6 +440,7 @@ private fun RenderReply(
innerQuote = true, innerQuote = true,
parentBackgroundColor = backgroundBubbleColor, parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )

View File

@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -48,6 +49,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
@@ -105,8 +107,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size34dp
@@ -122,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyBackground import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AppDefinitionEvent
@@ -238,7 +244,7 @@ fun AcceptableNote(
nav = nav, nav = nav,
) )
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
showPopup, showPopup,
-> ->
CheckNewAndRenderNote( CheckNewAndRenderNote(
@@ -273,7 +279,7 @@ fun AcceptableNote(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
showPopup, showPopup,
-> ->
CheckNewAndRenderNote( CheckNewAndRenderNote(
@@ -868,6 +874,29 @@ fun DisplayOtsIfInOriginal(
} }
} }
@Composable
fun DisplayDraft() {
Text(
"Draft",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
modifier = HalfStartPadding,
)
}
@Composable
fun DisplayDraftChat() {
Text(
"Draft",
color = MaterialTheme.colorScheme.placeholderText,
modifier = HalfEndPadding,
fontWeight = FontWeight.Bold,
fontSize = Font12SP,
maxLines = 1,
)
}
@Composable @Composable
fun FirstUserInfoRow( fun FirstUserInfoRow(
baseNote: Note, baseNote: Note,
@@ -910,6 +939,10 @@ fun FirstUserInfoRow(
} }
} }
if (baseNote.isDraft()) {
DisplayDraft()
}
TimeAgo(baseNote) TimeAgo(baseNote)
MoreOptionsButton(baseNote, editState, accountViewModel, nav) MoreOptionsButton(baseNote, editState, accountViewModel, nav)

View File

@@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FormatQuote import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonRemove import androidx.compose.material.icons.filled.PersonRemove
@@ -84,6 +85,8 @@ import androidx.core.graphics.ColorUtils
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
@@ -132,6 +135,7 @@ val externalLinkForNote = { note: Note ->
fun LongPressToQuickAction( fun LongPressToQuickAction(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
content: @Composable (() -> Unit) -> Unit, content: @Composable (() -> Unit) -> Unit,
) { ) {
val popupExpanded = remember { mutableStateOf(false) } val popupExpanded = remember { mutableStateOf(false) }
@@ -140,7 +144,7 @@ fun LongPressToQuickAction(
content(showPopup) content(showPopup)
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel) NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel, newPostViewModel)
} }
@Composable @Composable
@@ -149,20 +153,24 @@ fun NoteQuickActionMenu(
popupExpanded: Boolean, popupExpanded: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
) { ) {
val showSelectTextDialog = remember { mutableStateOf(false) } val showSelectTextDialog = remember { mutableStateOf(false) }
val showDeleteAlertDialog = remember { mutableStateOf(false) } val showDeleteAlertDialog = remember { mutableStateOf(false) }
val showBlockAlertDialog = remember { mutableStateOf(false) } val showBlockAlertDialog = remember { mutableStateOf(false) }
val showReportDialog = remember { mutableStateOf(false) } val showReportDialog = remember { mutableStateOf(false) }
val editDraftDialog = remember { mutableStateOf(false) }
if (popupExpanded) { if (popupExpanded) {
RenderMainPopup( RenderMainPopup(
accountViewModel, accountViewModel,
newPostViewModel,
note, note,
onDismiss, onDismiss,
showBlockAlertDialog, showBlockAlertDialog,
showDeleteAlertDialog, showDeleteAlertDialog,
showReportDialog, showReportDialog,
editDraftDialog,
) )
} }
@@ -199,16 +207,29 @@ fun NoteQuickActionMenu(
onDismiss() onDismiss()
} }
} }
if (editDraftDialog.value) {
NewPostView(
onClose = {
editDraftDialog.value = false
},
accountViewModel = accountViewModel,
draft = note,
nav = { },
)
}
} }
@Composable @Composable
private fun RenderMainPopup( private fun RenderMainPopup(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
note: Note, note: Note,
onDismiss: () -> Unit, onDismiss: () -> Unit,
showBlockAlertDialog: MutableState<Boolean>, showBlockAlertDialog: MutableState<Boolean>,
showDeleteAlertDialog: MutableState<Boolean>, showDeleteAlertDialog: MutableState<Boolean>,
showReportDialog: MutableState<Boolean>, showReportDialog: MutableState<Boolean>,
editDraftDialog: MutableState<Boolean>,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f) val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
@@ -279,6 +300,22 @@ private fun RenderMainPopup(
} }
} }
if (note.isDraft()) {
VerticalDivider(color = primaryLight)
NoteQuickActionItem(
Icons.Default.Edit,
stringResource(R.string.edit_draft),
) {
if (newPostViewModel != null) {
newPostViewModel.load(accountViewModel, null, null, null, null, note)
onDismiss()
} else {
editDraftDialog.value = true
onDismiss()
}
}
}
if (!isOwnNote) { if (!isOwnNote) {
VerticalDivider(color = primaryLight) VerticalDivider(color = primaryLight)
@@ -389,14 +426,20 @@ fun NoteQuickActionItem(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Column( Column(
modifier = Modifier.size(70.dp).clickable { onClick() }, modifier =
Modifier
.size(70.dp)
.clickable { onClick() },
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp).padding(bottom = 5.dp), modifier =
Modifier
.size(24.dp)
.padding(bottom = 5.dp),
tint = Color.White, tint = Color.White,
) )
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center) Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
@@ -527,7 +570,10 @@ fun QuickActionAlertDialog(
text = { Text(textContent) }, text = { Text(textContent) },
confirmButton = { confirmButton = {
Row( Row(
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(), modifier =
Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
TextButton(onClick = onClickDontShowAgain) { TextButton(onClick = onClickDontShowAgain) {

View File

@@ -310,7 +310,12 @@ fun RenderZapRaiser(
} }
LinearProgressIndicator( LinearProgressIndicator(
modifier = remember(details) { Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp) }, modifier =
remember(details) {
Modifier
.fillMaxWidth()
.height(if (details) 24.dp else 4.dp)
},
color = color, color = color,
progress = { zapraiserStatus.progress }, progress = { zapraiserStatus.progress },
) )
@@ -590,6 +595,13 @@ fun ReplyReaction(
IconButton( IconButton(
modifier = iconSizeModifier, modifier = iconSizeModifier,
onClick = { onClick = {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_reply_to_a_draft_note,
)
return@IconButton
}
if (accountViewModel.isWriteable()) { if (accountViewModel.isWriteable()) {
onPress() onPress()
} else { } else {
@@ -776,7 +788,8 @@ fun LikeReaction(
Box( Box(
contentAlignment = Center, contentAlignment = Center,
modifier = modifier =
Modifier.size(iconSize) Modifier
.size(iconSize)
.combinedClickable( .combinedClickable(
role = Role.Button, role = Role.Button,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
@@ -784,6 +797,7 @@ fun LikeReaction(
onClick = { onClick = {
likeClick( likeClick(
accountViewModel, accountViewModel,
baseNote,
onMultipleChoices = { wantsToReact = true }, onMultipleChoices = { wantsToReact = true },
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) }, onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
) )
@@ -886,9 +900,17 @@ fun ObserveLikeText(
private fun likeClick( private fun likeClick(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
baseNote: Note,
onMultipleChoices: () -> Unit, onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit, onWantsToSignReaction: () -> Unit,
) { ) {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_react_to_a_draft_note,
)
return
}
if (accountViewModel.account.reactionChoices.isEmpty()) { if (accountViewModel.account.reactionChoices.isEmpty()) {
accountViewModel.toast( accountViewModel.toast(
R.string.no_reactions_setup, R.string.no_reactions_setup,
@@ -1082,6 +1104,14 @@ fun zapClick(
onError: (String, String) -> Unit, onError: (String, String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note,
)
return
}
if (accountViewModel.account.zapAmountChoices.isEmpty()) { if (accountViewModel.account.zapAmountChoices.isEmpty()) {
accountViewModel.toast( accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error), context.getString(R.string.error_dialog_zap_error),

View File

@@ -52,6 +52,7 @@ fun WatchNoteEvent(
LongPressToQuickAction( LongPressToQuickAction(
baseNote = baseNote, baseNote = baseNote,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = null,
) { showPopup -> ) { showPopup ->
BlankNote( BlankNote(
remember { remember {

View File

@@ -179,7 +179,7 @@ fun ZapCustomDialog(
) )
ZapButton( ZapButton(
isActive = postViewModel.canSend(), isActive = postViewModel.canSend() && !baseNote.isDraft(),
) { ) {
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,

View File

@@ -185,6 +185,7 @@ class AddBountyAmountViewModel : ViewModel() {
root = null, root = null,
directMentions = setOf(), directMentions = setOf(),
forkedFrom = null, forkedFrom = null,
draftTag = null,
) )
nextAmount = TextFieldValue("") nextAmount = TextFieldValue("")

View File

@@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
@@ -122,6 +123,11 @@ fun NoteDropDownMenu(
mutableStateOf(false) mutableStateOf(false)
} }
val wantsToEditDraft =
remember {
mutableStateOf(false)
}
if (wantsToEditPost.value) { if (wantsToEditPost.value) {
// avoids changing while drafting a note and a new event shows up. // avoids changing while drafting a note and a new event shows up.
val versionLookingAt = val versionLookingAt =
@@ -141,6 +147,18 @@ fun NoteDropDownMenu(
) )
} }
if (wantsToEditDraft.value) {
NewPostView(
onClose = {
popupExpanded.value = false
wantsToEditDraft.value = false
},
accountViewModel = accountViewModel,
draft = note,
nav = nav,
)
}
DropdownMenu( DropdownMenu(
expanded = popupExpanded.value, expanded = popupExpanded.value,
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -219,7 +237,15 @@ fun NoteDropDownMenu(
}, },
) )
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
if (note.event is TextNoteEvent) { if (note.isDraft()) {
DropdownMenuItem(
text = { Text(stringResource(R.string.edit_draft)) },
onClick = {
wantsToEditDraft.value = true
},
)
}
if (note.event is TextNoteEvent && !note.isDraft()) {
if (state.isLoggedUser) { if (state.isLoggedUser) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.edit_post)) }, text = { Text(stringResource(R.string.edit_post)) },

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
fun RefreshingChatroomFeedView( fun RefreshingChatroomFeedView(
viewModel: FeedViewModel, viewModel: FeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
@@ -59,6 +61,7 @@ fun RefreshingChatroomFeedView(
RenderChatroomFeedView( RenderChatroomFeedView(
viewModel, viewModel,
accountViewModel, accountViewModel,
newPostViewModel,
listState, listState,
nav, nav,
routeForLastRead, routeForLastRead,
@@ -72,6 +75,7 @@ fun RefreshingChatroomFeedView(
fun RenderChatroomFeedView( fun RenderChatroomFeedView(
viewModel: FeedViewModel, viewModel: FeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
@@ -91,6 +95,7 @@ fun RenderChatroomFeedView(
ChatroomFeedLoaded( ChatroomFeedLoaded(
state, state,
accountViewModel, accountViewModel,
newPostViewModel,
listState, listState,
nav, nav,
routeForLastRead, routeForLastRead,
@@ -108,6 +113,7 @@ fun RenderChatroomFeedView(
fun ChatroomFeedLoaded( fun ChatroomFeedLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
@@ -130,6 +136,7 @@ fun ChatroomFeedLoaded(
baseNote = item, baseNote = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )

View File

@@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
@@ -446,6 +447,10 @@ fun NoteMaster(
DisplayPoW(pow) DisplayPoW(pow)
} }
if (note.isDraft()) {
DisplayDraft()
}
DisplayOtsIfInOriginal(note, editState, accountViewModel) DisplayOtsIfInOriginal(note, editState, accountViewModel)
} }
} }
@@ -605,7 +610,7 @@ fun NoteMaster(
ReactionsRow(note, true, editState, accountViewModel, nav) ReactionsRow(note, true, editState, accountViewModel, nav)
} }
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel, null)
} }
} }

View File

@@ -1209,6 +1209,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
baseNote: Note, baseNote: Note,
onMore: () -> Unit, onMore: () -> Unit,
) { ) {
if (baseNote.isDraft()) {
toast(
R.string.draft_note,
R.string.it_s_not_possible_to_quote_to_a_draft_note,
)
return
}
if (isWriteable()) { if (isWriteable()) {
if (hasBoosted(baseNote)) { if (hasBoosted(baseNote)) {
deleteBoostsTo(baseNote) deleteBoostsTo(baseNote)
@@ -1304,6 +1312,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
} }
suspend fun deleteDraft(draftTag: String) {
val notes = LocalCache.draftNotes(draftTag)
account.delete(notes)
}
val bechLinkCache = CachedLoadedBechLink(this) val bechLinkCache = CachedLoadedBechLink(this)
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) { class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {

View File

@@ -163,12 +163,17 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.UUID
@Composable @Composable
fun ChannelScreen( fun ChannelScreen(
@@ -187,6 +192,7 @@ fun ChannelScreen(
} }
} }
@OptIn(FlowPreview::class)
@Composable @Composable
fun PrepareChannelViewModels( fun PrepareChannelViewModels(
baseChannel: Channel, baseChannel: Channel,
@@ -204,8 +210,21 @@ fun PrepareChannelViewModels(
) )
val channelScreenModel: NewPostViewModel = viewModel() val channelScreenModel: NewPostViewModel = viewModel()
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
channelScreenModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
channelScreenModel.sendPost(localDraft = channelScreenModel.draftTag)
}
}
}
channelScreenModel.accountViewModel = accountViewModel channelScreenModel.accountViewModel = accountViewModel
channelScreenModel.account = accountViewModel.account channelScreenModel.account = accountViewModel.account
channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex)
ChannelScreen( ChannelScreen(
channel = baseChannel, channel = baseChannel,
@@ -287,6 +306,7 @@ fun ChannelScreen(
RefreshingChatroomFeedView( RefreshingChatroomFeedView(
viewModel = feedViewModel, viewModel = feedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
routeForLastRead = "Channel/${channel.idHex}", routeForLastRead = "Channel/${channel.idHex}",
onWantsToReply = { replyTo.value = it }, onWantsToReply = { replyTo.value = it },
@@ -295,7 +315,7 @@ fun ChannelScreen(
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } } replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -323,6 +343,7 @@ fun ChannelScreen(
mentions = tagger.pTags, mentions = tagger.pTags,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null,
) )
} else if (channel is LiveActivitiesChannel) { } else if (channel is LiveActivitiesChannel) {
accountViewModel.account.sendLiveMessage( accountViewModel.account.sendLiveMessage(
@@ -332,10 +353,13 @@ fun ChannelScreen(
mentions = tagger.pTags, mentions = tagger.pTags,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null,
) )
} }
newPostModel.message = TextFieldValue("") newPostModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
feedViewModel.sendToTop() feedViewModel.sendToTop()
} }
} }
@@ -346,6 +370,7 @@ fun ChannelScreen(
fun DisplayReplyingToNote( fun DisplayReplyingToNote(
replyingNote: Note?, replyingNote: Note?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostModel: NewPostViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
@@ -364,6 +389,7 @@ fun DisplayReplyingToNote(
null, null,
innerQuote = true, innerQuote = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
onWantsToReply = {}, onWantsToReply = {},
) )
@@ -665,7 +691,12 @@ fun ShowVideoStreaming(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = remember { Modifier.fillMaxWidth().heightIn(min = 50.dp, max = 300.dp) }, modifier =
remember {
Modifier
.fillMaxWidth()
.heightIn(min = 50.dp, max = 300.dp)
},
) { ) {
val zoomableUrlVideo = val zoomableUrlVideo =
remember(streamingInfo) { remember(streamingInfo) {

View File

@@ -116,14 +116,21 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.findURLs
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.UUID
@Composable @Composable
fun ChatroomScreen( fun ChatroomScreen(
@@ -206,6 +213,7 @@ fun LoadRoomByAuthor(
content(room) content(room)
} }
@OptIn(FlowPreview::class)
@Composable @Composable
fun PrepareChatroomViewModels( fun PrepareChatroomViewModels(
room: ChatroomKey, room: ChatroomKey,
@@ -230,8 +238,20 @@ fun PrepareChatroomViewModels(
if (newPostModel.requiresNIP24) { if (newPostModel.requiresNIP24) {
newPostModel.nip24 = true newPostModel.nip24 = true
} }
room.users.forEach {
newPostModel.toUsers = TextFieldValue(newPostModel.toUsers.text + " @${Hex.decode(it).toNpub()}")
}
LaunchedEffect(key1 = newPostModel) { LaunchedEffect(key1 = newPostModel) {
launch(Dispatchers.IO) {
newPostModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
newPostModel.sendPost(localDraft = newPostModel.draftTag)
}
}
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val hasNIP24 = val hasNIP24 =
accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any { accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any {
@@ -313,21 +333,28 @@ fun ChatroomScreen(
RefreshingChatroomFeedView( RefreshingChatroomFeedView(
viewModel = feedViewModel, viewModel = feedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
routeForLastRead = "Room/${room.hashCode()}", routeForLastRead = "Room/${room.hashCode()}",
onWantsToReply = { replyTo.value = it }, onWantsToReply = {
replyTo.value = it
newPostModel.originalNote = it
},
) )
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } } replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// LAST ROW // LAST ROW
PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) { PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
val urls = findURLs(newPostModel.message.text) val urls = findURLs(newPostModel.message.text)
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
@@ -339,6 +366,7 @@ fun ChatroomScreen(
mentions = null, mentions = null,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null,
) )
} else { } else {
accountViewModel.account.sendPrivateMessage( accountViewModel.account.sendPrivateMessage(
@@ -348,6 +376,7 @@ fun ChatroomScreen(
mentions = null, mentions = null,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null,
) )
} }

View File

@@ -93,6 +93,7 @@ val Size40dp = 40.dp
val Size55dp = 55.dp val Size55dp = 55.dp
val Size75dp = 75.dp val Size75dp = 75.dp
val HalfEndPadding = Modifier.padding(end = 5.dp)
val HalfStartPadding = Modifier.padding(start = 5.dp) val HalfStartPadding = Modifier.padding(start = 5.dp)
val StdStartPadding = Modifier.padding(start = 10.dp) val StdStartPadding = Modifier.padding(start = 10.dp)
val StdTopPadding = Modifier.padding(top = 10.dp) val StdTopPadding = Modifier.padding(top = 10.dp)

View File

@@ -733,6 +733,8 @@
<string name="could_not_download_from_the_server">Could not download uploaded media from the server</string> <string name="could_not_download_from_the_server">Could not download uploaded media from the server</string>
<string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string> <string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string>
<string name="edit_draft">Edit draft</string>
<string name="login_with_qr_code">Login with QR Code</string> <string name="login_with_qr_code">Login with QR Code</string>
<string name="route">Route</string> <string name="route">Route</string>
<string name="route_home">Home</string> <string name="route_home">Home</string>
@@ -819,4 +821,9 @@
<string name="accessibility_play_username">Play username as audio</string> <string name="accessibility_play_username">Play username as audio</string>
<string name="accessibility_scan_qr_code">Scan QR code</string> <string name="accessibility_scan_qr_code">Scan QR code</string>
<string name="accessibility_navigate_to_alby">Navigate to the third-party wallet provider Alby</string> <string name="accessibility_navigate_to_alby">Navigate to the third-party wallet provider Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">It\'s not possible to reply a draft note</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">It\'s not possible to quote a draft note</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">It\'s not possible to react a draft note</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">It\'s not possible to zap a draft note</string>
<string name="draft_note">Draft Note</string>
</resources> </resources>

View File

@@ -60,6 +60,7 @@ class ChannelMessageEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
isDraft: Boolean,
onReady: (ChannelMessageEvent) -> Unit, onReady: (ChannelMessageEvent) -> Unit,
) { ) {
val tags = val tags =
@@ -87,7 +88,7 @@ class ChannelMessageEvent(
arrayOf("alt", ALT), arrayOf("alt", ALT),
) )
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft)
} }
} }
} }

View File

@@ -82,6 +82,7 @@ class ChatMessageEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
isDraft: Boolean,
onReady: (ChatMessageEvent) -> Unit, onReady: (ChatMessageEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
@@ -106,7 +107,7 @@ class ChatMessageEvent(
} }
// tags.add(arrayOf("alt", alt)) // tags.add(arrayOf("alt", alt))
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
} }
} }
} }

View File

@@ -113,6 +113,7 @@ class ClassifiedsEvent(
nip94attachments: List<Event>? = null, nip94attachments: List<Event>? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (ClassifiedsEvent) -> Unit, onReady: (ClassifiedsEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
@@ -192,7 +193,7 @@ class ClassifiedsEvent(
} }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft)
} }
} }
} }

View File

@@ -0,0 +1,161 @@
/**
* Copyright (c) 2024 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.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class DraftEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var decryptedContent: Map<HexKey, Event> = mapOf()
@Transient private var citedNotesCache: Set<String>? = null
fun replyTos(): List<HexKey> {
val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] }
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
val newStyleReplyTos = listOfNotNull(newStyleReply, newStyleRoot)
return if (newStyleReplyTos.isNotEmpty()) {
newStyleReplyTos
} else {
oldStylePositional
}
}
fun findCitations(): Set<HexKey> {
citedNotesCache?.let {
return it
}
val citations = mutableSetOf<HexKey>()
// Removes citations from replies:
val matcher = tagSearch.matcher(content)
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { tags[it.toInt()] }
if (tag != null && tag.size > 1 && tag[0] == "e") {
citations.add(tag[1])
}
if (tag != null && tag.size > 1 && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) {
}
}
val matcher2 = Nip19Bech32.nip19regex.matcher(content)
while (matcher2.find()) {
val type = matcher2.group(2) // npub1
val key = matcher2.group(3) // bech32
val additionalChars = matcher2.group(4) // additional chars
if (type != null) {
val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity
if (parsed != null) {
when (parsed) {
is Nip19Bech32.NEvent -> citations.add(parsed.hex)
is Nip19Bech32.NAddress -> citations.add(parsed.atag)
is Nip19Bech32.Note -> citations.add(parsed.hex)
is Nip19Bech32.NEmbed -> citations.add(parsed.event.id)
}
}
}
}
citedNotesCache = citations
return citations
}
fun tagsWithoutCitations(): List<String> {
val repliesTo = replyTos()
val tagAddresses =
taggedAddresses().filter {
it.kind != CommunityDefinitionEvent.KIND &&
it.kind != WikiNoteEvent.KIND
}.map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations()
return if (citations.isEmpty()) {
repliesTo + tagAddresses
} else {
repliesTo.filter { it !in citations }
}
}
fun cachedContentFor(): Event? {
return decryptedContent[dTag()]
}
fun plainContent(
signer: NostrSigner,
onReady: (Event) -> Unit,
) {
decryptedContent[dTag()]?.let {
onReady(it)
return
}
signer.nip44Decrypt(content, signer.pubKey) { retVal ->
val event = runCatching { fromJson(retVal) }.getOrNull() ?: return@nip44Decrypt
decryptedContent = decryptedContent + Pair(dTag(), event)
onReady(event)
}
}
companion object {
const val KIND = 31234
fun create(
dTag: String,
originalNote: EventInterface,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", dTag))
tags.add(arrayOf("k", "${originalNote.kind()}"))
tags.addAll(originalNote.tags().filter { it.size > 1 && it[0] == "e" })
tags.addAll(originalNote.tags().filter { it.size > 1 && it[0] == "a" })
signer.nip44Encrypt(originalNote.toJson(), signer.pubKey) { encryptedContent ->
signer.sign(createdAt, KIND, tags.toTypedArray(), encryptedContent, onReady)
}
}
}
}

View File

@@ -79,6 +79,7 @@ class EventFactory {
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent.KIND ->
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)

View File

@@ -94,6 +94,7 @@ class GitReplyEvent(
forkedFrom: Event? = null, forkedFrom: Event? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (GitReplyEvent) -> Unit, onReady: (GitReplyEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
@@ -156,7 +157,7 @@ class GitReplyEvent(
} }
tags.add(arrayOf("alt", "a git issue reply")) tags.add(arrayOf("alt", "a git issue reply"))
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
} }
} }
} }

View File

@@ -72,6 +72,7 @@ class LiveActivitiesChatMessageEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
isDraft: Boolean,
onReady: (LiveActivitiesChatMessageEvent) -> Unit, onReady: (LiveActivitiesChatMessageEvent) -> Unit,
) { ) {
val content = message val content = message
@@ -98,7 +99,7 @@ class LiveActivitiesChatMessageEvent(
} }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft)
} }
} }
} }

View File

@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.quartz.events package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
@@ -78,6 +77,7 @@ class NIP24Factory {
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String? = null,
onReady: (Result) -> Unit, onReady: (Result) -> Unit,
) { ) {
val senderPublicKey = signer.pubKey val senderPublicKey = signer.pubKey
@@ -93,15 +93,25 @@ class NIP24Factory {
markAsSensitive = markAsSensitive, markAsSensitive = markAsSensitive,
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
geohash = geohash, geohash = geohash,
isDraft = draftTag != null,
nip94attachments = nip94attachments, nip94attachments = nip94attachments,
) { senderMessage -> ) { senderMessage ->
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> if (draftTag != null) {
onReady( onReady(
Result( Result(
msg = senderMessage, msg = senderMessage,
wraps = wraps, wraps = listOf(),
), ),
) )
} else {
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
onReady(
Result(
msg = senderMessage,
wraps = wraps,
),
)
}
} }
} }
} }
@@ -155,49 +165,4 @@ class NIP24Factory {
} }
} }
} }
fun createTextNoteNIP24(
msg: String,
to: List<HexKey>,
signer: NostrSigner,
replyTos: List<String>? = null,
mentions: List<String>? = null,
addresses: List<ATag>?,
extraTags: List<String>?,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
replyingTo: String?,
root: String?,
directMentions: Set<HexKey>,
zapRaiserAmount: Long? = null,
geohash: String? = null,
onReady: (Result) -> Unit,
) {
val senderPublicKey = signer.pubKey
TextNoteEvent.create(
msg = msg,
signer = signer,
replyTos = replyTos,
mentions = mentions,
zapReceiver = zapReceiver,
root = root,
extraTags = extraTags,
addresses = addresses,
directMentions = directMentions,
replyingTo = replyingTo,
markAsSensitive = markAsSensitive,
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
) { senderMessage ->
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
onReady(
Result(
msg = senderMessage,
wraps = wraps,
),
)
}
}
}
} }

View File

@@ -80,6 +80,7 @@ class PollNoteEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
isDraft: Boolean,
onReady: (PollNoteEvent) -> Unit, onReady: (PollNoteEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
@@ -112,7 +113,7 @@ class PollNoteEvent(
} }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
} }
} }
} }

View File

@@ -126,6 +126,7 @@ class PrivateDmEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null, nip94attachments: List<FileHeaderEvent>? = null,
isDraft: Boolean,
onReady: (PrivateDmEvent) -> Unit, onReady: (PrivateDmEvent) -> Unit,
) { ) {
var message = msg var message = msg
@@ -167,7 +168,7 @@ class PrivateDmEvent(
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
signer.nip04Encrypt(message, recipientPubKey) { content -> signer.nip04Encrypt(message, recipientPubKey) { content ->
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft)
} }
} }
} }

View File

@@ -60,6 +60,7 @@ class TextNoteEvent(
forkedFrom: Event? = null, forkedFrom: Event? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (TextNoteEvent) -> Unit, onReady: (TextNoteEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
@@ -124,7 +125,7 @@ class TextNoteEvent(
} }
} }
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
} }
} }
} }

View File

@@ -171,6 +171,10 @@ class ExternalSignerLauncher(
"sign_event", "sign_event",
22242, 22242,
), ),
Permission(
"sign_event",
31234,
),
Permission( Permission(
"nip04_encrypt", "nip04_encrypt",
), ),

View File

@@ -32,6 +32,7 @@ abstract class NostrSigner(val pubKey: HexKey) {
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
onReady: (T) -> Unit, onReady: (T) -> Unit,
isDraft: Boolean = false,
) )
abstract fun nip04Encrypt( abstract fun nip04Encrypt(

View File

@@ -40,7 +40,13 @@ class NostrSignerExternal(
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
onReady: (T) -> Unit, onReady: (T) -> Unit,
isDraft: Boolean,
) { ) {
if (isDraft) {
unsignedEvent(createdAt, kind, tags, content, onReady)
return
}
val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey()
val event = val event =
@@ -86,6 +92,28 @@ class NostrSignerExternal(
} }
} }
fun <T : Event> unsignedEvent(
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
onReady: (T) -> Unit,
) {
val id = Event.generateId(pubKey, createdAt, kind, tags, content)
onReady(
EventFactory.create(
id.toHexKey(),
pubKey,
createdAt,
kind,
tags,
content,
"",
) as T,
)
}
override fun nip04Encrypt( override fun nip04Encrypt(
decryptedContent: String, decryptedContent: String,
toPublicKey: HexKey, toPublicKey: HexKey,

View File

@@ -38,9 +38,15 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
onReady: (T) -> Unit, onReady: (T) -> Unit,
isDraft: Boolean,
) { ) {
if (keyPair.privKey == null) return if (keyPair.privKey == null) return
if (isDraft) {
unsignedEvent(createdAt, kind, tags, content, onReady)
return
}
if (isUnsignedPrivateEvent(kind, tags)) { if (isUnsignedPrivateEvent(kind, tags)) {
// this is a private zap // this is a private zap
signPrivateZap(createdAt, kind, tags, content, onReady) signPrivateZap(createdAt, kind, tags, content, onReady)
@@ -82,6 +88,30 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
) )
} }
fun <T : Event> unsignedEvent(
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
onReady: (T) -> Unit,
) {
if (keyPair.privKey == null) return
val id = Event.generateId(pubKey, createdAt, kind, tags, content)
onReady(
EventFactory.create(
id.toHexKey(),
pubKey,
createdAt,
kind,
tags,
content,
"",
) as T,
)
}
override fun nip04Encrypt( override fun nip04Encrypt(
decryptedContent: String, decryptedContent: String,
toPublicKey: HexKey, toPublicKey: HexKey,