mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-12 13:59:29 +02:00
Merge pull request #749 from greenart7c3/main
save a draft while you are typing the post
This commit is contained in:
commit
d3a0ae743a
@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.Contact
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
@ -845,7 +846,14 @@ class Account(
|
||||
}
|
||||
|
||||
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>) {
|
||||
@ -897,10 +905,17 @@ class Account(
|
||||
|
||||
fun broadcast(note: Note) {
|
||||
note.event?.let {
|
||||
if (it is WrappedEvent && it.host != null) {
|
||||
it.host?.let { hostEvent -> Client.send(hostEvent) }
|
||||
if (note.isDraft()) {
|
||||
val drafts = LocalCache.getDrafts(it.id())
|
||||
drafts.forEach { draftNote ->
|
||||
broadcast(draftNote)
|
||||
}
|
||||
} 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) {
|
||||
if (!isWriteable()) return
|
||||
if (note.isDraft()) return
|
||||
|
||||
val id = note.event?.id() ?: note.idHex
|
||||
|
||||
@ -1318,6 +1334,7 @@ class Account(
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1345,14 +1362,24 @@ class Account(
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != 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) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1373,6 +1400,7 @@ class Account(
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1396,20 +1424,30 @@ class Account(
|
||||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
if (draftTag != 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())
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// 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,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1453,20 +1492,30 @@ class Account(
|
||||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
if (draftTag != 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())
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// 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,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1533,15 +1583,25 @@ class Account(
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != 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
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
// Rebroadcast replies and tags to the current relay set
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1557,6 +1617,7 @@ class Account(
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1574,9 +1635,19 @@ class Account(
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != 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,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1608,9 +1680,19 @@ class Account(
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.justConsume(it, null)
|
||||
if (draftTag != 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,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
sendPrivateMessage(
|
||||
message,
|
||||
@ -1635,6 +1718,7 @@ class Account(
|
||||
zapRaiserAmount,
|
||||
geohash,
|
||||
nip94attachments,
|
||||
draftTag,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1648,6 +1732,7 @@ class Account(
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1667,9 +1752,19 @@ class Account(
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer,
|
||||
advertiseNip18 = false,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it)
|
||||
LocalCache.consume(it, null)
|
||||
if (draftTag != 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,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String? = null,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1701,9 +1797,19 @@ class Account(
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
draftTag = draftTag,
|
||||
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,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
if (note.isDraft()) return
|
||||
|
||||
if (note is AddressableNote) {
|
||||
BookmarkListEvent.addReplaceable(
|
||||
@ -2218,6 +2325,7 @@ class Account(
|
||||
|
||||
fun cachedDecryptContent(note: Note): String? {
|
||||
val event = note.event
|
||||
|
||||
return if (event is PrivateDmEvent && isWriteable()) {
|
||||
event.cachedContentFor(signer)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
||||
|
23
app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt
Normal file
23
app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt
Normal 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)
|
@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.events.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
@ -128,7 +129,7 @@ object LocalCache {
|
||||
val users = LargeCache<HexKey, User>()
|
||||
val notes = LargeCache<HexKey, Note>()
|
||||
val addressables = LargeCache<String, AddressableNote>()
|
||||
|
||||
val drafts = ConcurrentHashMap<String, MutableList<Drafts>>()
|
||||
val channels = LargeCache<HexKey, Channel>()
|
||||
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
@ -141,6 +142,34 @@ object LocalCache {
|
||||
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 {
|
||||
// checkNotInMainThread()
|
||||
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(
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
@ -2050,6 +2086,7 @@ object LocalCache {
|
||||
}
|
||||
is ContactListEvent -> consume(event)
|
||||
is DeletionEvent -> consume(event)
|
||||
is DraftEvent -> consume(event, relay)
|
||||
is EmojiPackEvent -> consume(event, relay)
|
||||
is EmojiPackSelectionEvent -> consume(event, relay)
|
||||
is SealedGossipEvent -> consume(event, relay)
|
||||
|
@ -184,6 +184,13 @@ open class Note(val idHex: String) {
|
||||
|
||||
open fun createdAt() = event?.createdAt()
|
||||
|
||||
fun isDraft(): Boolean {
|
||||
event?.let {
|
||||
return it.sig().isBlank()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun loadEvent(
|
||||
event: Event,
|
||||
author: User,
|
||||
|
@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
||||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
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() =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
@ -262,22 +273,46 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
if (event 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
|
||||
when (event) {
|
||||
is DraftEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
|
||||
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) {
|
||||
// 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
|
||||
LocalCache.justConsume(event, relay)
|
||||
event.plainContent(account.signer) {
|
||||
val tag =
|
||||
event.tags().filter { it.size > 1 && it[0] == "d" }.map {
|
||||
it[1]
|
||||
}.firstOrNull()
|
||||
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
} else {
|
||||
LocalCache.justConsume(event, relay)
|
||||
LocalCache.justConsume(it, relay)
|
||||
tag?.let { lTag ->
|
||||
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(),
|
||||
createAccountLastPostsListFilter(),
|
||||
createOtherAccountsBaseFilter(),
|
||||
createDraftsFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
} else {
|
||||
|
@ -34,8 +34,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewPollOption(
|
||||
@ -45,7 +48,12 @@ fun NewPollOption(
|
||||
Row {
|
||||
val deleteIcon: @Composable (() -> Unit) = {
|
||||
IconButton(
|
||||
onClick = { pollViewModel.pollOptions.remove(optionIndex) },
|
||||
onClick = {
|
||||
pollViewModel.pollOptions.remove(optionIndex)
|
||||
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
pollViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
@ -57,7 +65,12 @@ fun NewPollOption(
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.weight(1F),
|
||||
value = pollViewModel.pollOptions[optionIndex] ?: "",
|
||||
onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
|
||||
onValueChange = {
|
||||
pollViewModel.pollOptions[optionIndex] = it
|
||||
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
pollViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
|
||||
|
@ -119,6 +119,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
@ -171,13 +172,18 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
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.withContext
|
||||
import java.lang.Math.round
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
onClose: () -> Unit,
|
||||
@ -185,6 +191,7 @@ fun NewPostView(
|
||||
quote: Note? = null,
|
||||
fork: Note? = null,
|
||||
version: Note? = null,
|
||||
draft: Note? = null,
|
||||
enableMessageInterface: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -200,9 +207,17 @@ fun NewPostView(
|
||||
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version)
|
||||
|
||||
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 ->
|
||||
withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
@ -582,6 +597,9 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
|
||||
|
||||
MarkAsSensitive(postViewModel) {
|
||||
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
|
||||
postViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
postViewModel.saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
AddGeoHash(postViewModel) {
|
||||
@ -827,7 +845,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
|
||||
MyTextField(
|
||||
value = postViewModel.title,
|
||||
onValueChange = { postViewModel.title = it },
|
||||
onValueChange = {
|
||||
postViewModel.title = it
|
||||
postViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
postViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@ -870,6 +893,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
postViewModel.price = it
|
||||
}
|
||||
}
|
||||
postViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
postViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
@ -934,7 +960,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
TextSpinner(
|
||||
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
|
||||
options = conditionOptions,
|
||||
onSelect = { postViewModel.condition = conditionTypes[it].first },
|
||||
onSelect = {
|
||||
postViewModel.condition = conditionTypes[it].first
|
||||
postViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
postViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
@ -998,7 +1029,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second
|
||||
?: "",
|
||||
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
|
||||
.weight(1f)
|
||||
@ -1033,7 +1069,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
|
||||
|
||||
MyTextField(
|
||||
value = postViewModel.locationText,
|
||||
onValueChange = { postViewModel.locationText = it },
|
||||
onValueChange = {
|
||||
postViewModel.locationText = it
|
||||
postViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
postViewModel.saveDraft()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
|
@ -69,10 +69,12 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
enum class UserSuggestionAnchor {
|
||||
MAIN_MESSAGE,
|
||||
@ -82,6 +84,7 @@ enum class UserSuggestionAnchor {
|
||||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var draftTag: String = UUID.randomUUID().toString()
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var requiresNIP24: Boolean = false
|
||||
@ -164,6 +167,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
// NIP24 Wrapped DMs / Group messages
|
||||
var nip24 by mutableStateOf(false)
|
||||
|
||||
val draftTextChanges = Channel<String>(Channel.CONFLATED)
|
||||
|
||||
fun lnAddress(): String? {
|
||||
return account?.userProfile()?.info?.lnAddress()
|
||||
}
|
||||
@ -182,127 +187,236 @@ open class NewPostViewModel() : ViewModel() {
|
||||
quote: Note?,
|
||||
fork: Note?,
|
||||
version: Note?,
|
||||
draft: Note?,
|
||||
) {
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
||||
} else {
|
||||
this.eTags = listOf(replyNote)
|
||||
}
|
||||
if (draft != null) {
|
||||
loadFromDraft(draft, accountViewModel)
|
||||
} else {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
|
||||
} else {
|
||||
this.eTags = listOf(replyNote)
|
||||
}
|
||||
|
||||
if (replyNote.event !is CommunityDefinitionEvent) {
|
||||
replyNote.author?.let { replyUser ->
|
||||
val currentMentions =
|
||||
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
if (replyNote.event !is CommunityDefinitionEvent) {
|
||||
replyNote.author?.let { replyUser ->
|
||||
val currentMentions =
|
||||
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
|
||||
if (currentMentions.contains(replyUser)) {
|
||||
this.pTags = currentMentions
|
||||
} else {
|
||||
this.pTags = currentMentions.plus(replyUser)
|
||||
if (currentMentions.contains(replyUser)) {
|
||||
this.pTags = currentMentions
|
||||
} else {
|
||||
this.pTags = currentMentions.plus(replyUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
eTags = null
|
||||
pTags = null
|
||||
?: run {
|
||||
eTags = 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
|
||||
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
|
||||
val localfowardZapTo = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zap" } ?: listOf()
|
||||
forwardZapTo = Split()
|
||||
localfowardZapTo.forEach {
|
||||
val user = LocalCache.getOrCreateUser(it[1])
|
||||
val value = it.last().toFloatOrNull() ?: 0f
|
||||
forwardZapTo.addItem(user, value)
|
||||
}
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
wantsToMarkAsSensitive = draft.event?.tags()?.any { it.size > 1 && it[0] == "content-warning" } ?: false
|
||||
wantsToAddGeoHash = draft.event?.tags()?.any { it.size > 1 && it[0] == "g" } ?: false
|
||||
val zapraiser = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zapraiser" } ?: listOf()
|
||||
wantsZapraiser = zapraiser.isNotEmpty()
|
||||
zapRaiserAmount = null
|
||||
if (wantsZapraiser) {
|
||||
zapRaiserAmount = zapraiser.first()[1].toLongOrNull() ?: 0
|
||||
}
|
||||
|
||||
fork?.let {
|
||||
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
it.event?.isSensitive()?.let {
|
||||
if (it) wantsToMarkAsSensitive = true
|
||||
eTags =
|
||||
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }?.mapNotNull {
|
||||
val note = LocalCache.checkGetOrCreateNote(it[1])
|
||||
note
|
||||
}
|
||||
|
||||
it.event?.zapraiserAmount()?.let {
|
||||
zapRaiserAmount = it
|
||||
pTags =
|
||||
draft.event?.tags()?.filter { it.size > 1 && it[0] == "p" }?.map {
|
||||
LocalCache.getOrCreateUser(it[1])
|
||||
}
|
||||
|
||||
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
|
||||
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "fork" }?.forEach {
|
||||
val note = LocalCache.checkGetOrCreateNote(it[1])
|
||||
forkedFromNote = note
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) }
|
||||
fun sendPost(
|
||||
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) {
|
||||
cancel()
|
||||
return
|
||||
@ -363,6 +477,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
account?.sendChannelMessage(
|
||||
@ -375,6 +490,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
} else if (originalNote?.event is PrivateDmEvent) {
|
||||
@ -388,6 +504,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (originalNote?.event is ChatMessageEvent) {
|
||||
val receivers =
|
||||
@ -423,6 +540,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
account?.sendPrivateMessage(
|
||||
@ -435,6 +553,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
} else if (originalNote?.event is GitIssueEvent) {
|
||||
@ -475,6 +594,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
if (wantsPoll) {
|
||||
@ -493,6 +613,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
relayList,
|
||||
geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else if (wantsProduct) {
|
||||
account?.sendClassifieds(
|
||||
@ -511,6 +632,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
} else {
|
||||
// adds markers
|
||||
@ -547,11 +669,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = localDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
if (localDraft == null) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun upload(
|
||||
@ -635,6 +759,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
urlPreview = null
|
||||
isUploadingImage = false
|
||||
pTags = null
|
||||
eTags = null
|
||||
|
||||
wantsDirectMessage = false
|
||||
|
||||
@ -663,6 +788,11 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
userSuggestionAnchor = null
|
||||
userSuggestionsMainMessage = null
|
||||
originalNote = null
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel?.deleteDraft(draftTag)
|
||||
}
|
||||
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
}
|
||||
@ -679,6 +809,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
pTags = pTags?.filter { it != userToRemove }
|
||||
}
|
||||
|
||||
open suspend fun saveDraft() {
|
||||
draftTextChanges.send("")
|
||||
}
|
||||
|
||||
open fun updateMessage(it: TextFieldValue) {
|
||||
message = it
|
||||
urlPreview = findUrlInMessage()
|
||||
@ -701,6 +835,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateToUsers(it: TextFieldValue) {
|
||||
@ -724,10 +862,16 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateZapForwardTo(it: TextFieldValue) {
|
||||
@ -754,6 +898,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
@ -799,6 +946,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestionsMainMessage = null
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
|
||||
@ -869,6 +1020,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
message = message.insertUrlAtCursor(imageUrl)
|
||||
urlPreview = findUrlInMessage()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
@ -913,6 +1067,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
@ -933,6 +1090,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
locUtil?.let {
|
||||
location =
|
||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||
viewModelScope.launch(Dispatchers.IO) { saveDraft() }
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
||||
}
|
||||
@ -957,6 +1115,11 @@ open class NewPostViewModel() : ViewModel() {
|
||||
} else {
|
||||
nip24 = !nip24
|
||||
}
|
||||
if (message.text.isNotBlank()) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMinZapAmountForPoll(textMin: String) {
|
||||
@ -976,6 +1139,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
checkMinMax()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMaxZapAmountForPoll(textMax: String) {
|
||||
@ -995,6 +1161,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
checkMinMax()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkMinMax() {
|
||||
|
@ -39,6 +39,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
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.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ZapRaiserRequest(
|
||||
@ -98,6 +101,9 @@ fun ZapRaiserRequest(
|
||||
} else {
|
||||
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
|
||||
}
|
||||
newPostViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
newPostViewModel.saveDraft()
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
|
@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
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.components.ClickableText
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
@ -159,7 +160,10 @@ fun DrawerContent(
|
||||
)
|
||||
|
||||
ListContent(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
drawerState,
|
||||
openSheet,
|
||||
accountViewModel,
|
||||
@ -231,7 +235,8 @@ fun ProfileContentTemplate(
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier =
|
||||
Modifier.width(100.dp)
|
||||
Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
|
||||
@ -244,7 +249,10 @@ fun ProfileContentTemplate(
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
modifier = Modifier.padding(top = 7.dp).clickable(onClick = onClick),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 7.dp)
|
||||
.clickable(onClick = onClick),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
@ -454,8 +462,17 @@ fun ListContent(
|
||||
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var draftText by remember {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
|
||||
var wantsToPost by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState()),
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
NavigationRow(
|
||||
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) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) },
|
||||
@ -662,7 +691,8 @@ fun IconRow(
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
@ -693,10 +723,16 @@ fun IconRowRelays(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onClick() },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
@ -737,7 +773,10 @@ fun BottomContent(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableText(
|
||||
|
@ -147,7 +147,7 @@ fun NormalChannelCard(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup ->
|
||||
CheckNewAndRenderChannelCard(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
|
@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
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.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
@ -102,6 +103,7 @@ fun ChatroomMessageCompose(
|
||||
innerQuote: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
) {
|
||||
@ -120,6 +122,7 @@ fun ChatroomMessageCompose(
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
nav,
|
||||
onWantsToReply,
|
||||
)
|
||||
@ -136,6 +139,7 @@ fun NormalChatNote(
|
||||
canPreview: Boolean = true,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
) {
|
||||
@ -255,6 +259,7 @@ fun NormalChatNote(
|
||||
availableBubbleSize,
|
||||
showDetails,
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
@ -265,6 +270,7 @@ fun NormalChatNote(
|
||||
popupExpanded = popupExpanded,
|
||||
onDismiss = { popupExpanded = false },
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -282,6 +288,7 @@ private fun RenderBubble(
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
showDetails: State<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val bubbleSize = remember { mutableIntStateOf(0) }
|
||||
@ -311,6 +318,7 @@ private fun RenderBubble(
|
||||
canPreview,
|
||||
showDetails,
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
@ -329,6 +337,7 @@ private fun MessageBubbleLines(
|
||||
canPreview: Boolean,
|
||||
showDetails: State<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
if (drawAuthorInfo) {
|
||||
@ -345,6 +354,7 @@ private fun MessageBubbleLines(
|
||||
innerQuote = innerQuote,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
)
|
||||
@ -363,6 +373,9 @@ private fun MessageBubbleLines(
|
||||
bubbleSize = bubbleSize,
|
||||
availableBubbleSize = availableBubbleSize,
|
||||
firstColumn = {
|
||||
if (baseNote.isDraft()) {
|
||||
DisplayDraftChat()
|
||||
}
|
||||
IncognitoBadge(baseNote)
|
||||
ChatTimeAgo(baseNote)
|
||||
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
|
||||
@ -394,11 +407,12 @@ private fun RenderReplyRow(
|
||||
innerQuote: Boolean,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
) {
|
||||
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,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
) {
|
||||
@ -425,6 +440,7 @@ private fun RenderReply(
|
||||
innerQuote = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
)
|
||||
|
@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.compositeOver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
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.theme.DoubleHorzSpacer
|
||||
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.HalfEndPadding
|
||||
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.Size30Modifier
|
||||
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.newItemBackgroundColor
|
||||
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.replyModifier
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
@ -238,7 +244,7 @@ fun AcceptableNote(
|
||||
nav = nav,
|
||||
)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
|
||||
showPopup,
|
||||
->
|
||||
CheckNewAndRenderNote(
|
||||
@ -273,7 +279,7 @@ fun AcceptableNote(
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
|
||||
showPopup,
|
||||
->
|
||||
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
|
||||
fun FirstUserInfoRow(
|
||||
baseNote: Note,
|
||||
@ -910,6 +939,10 @@ fun FirstUserInfoRow(
|
||||
}
|
||||
}
|
||||
|
||||
if (baseNote.isDraft()) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
TimeAgo(baseNote)
|
||||
|
||||
MoreOptionsButton(baseNote, editState, accountViewModel, nav)
|
||||
|
@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.AlternateEmail
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
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.PersonAdd
|
||||
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.model.AddressableNote
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
@ -132,6 +135,7 @@ val externalLinkForNote = { note: Note ->
|
||||
fun LongPressToQuickAction(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
content: @Composable (() -> Unit) -> Unit,
|
||||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
@ -140,7 +144,7 @@ fun LongPressToQuickAction(
|
||||
|
||||
content(showPopup)
|
||||
|
||||
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel)
|
||||
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel, newPostViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -149,20 +153,24 @@ fun NoteQuickActionMenu(
|
||||
popupExpanded: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
) {
|
||||
val showSelectTextDialog = remember { mutableStateOf(false) }
|
||||
val showDeleteAlertDialog = remember { mutableStateOf(false) }
|
||||
val showBlockAlertDialog = remember { mutableStateOf(false) }
|
||||
val showReportDialog = remember { mutableStateOf(false) }
|
||||
val editDraftDialog = remember { mutableStateOf(false) }
|
||||
|
||||
if (popupExpanded) {
|
||||
RenderMainPopup(
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
note,
|
||||
onDismiss,
|
||||
showBlockAlertDialog,
|
||||
showDeleteAlertDialog,
|
||||
showReportDialog,
|
||||
editDraftDialog,
|
||||
)
|
||||
}
|
||||
|
||||
@ -199,16 +207,29 @@ fun NoteQuickActionMenu(
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (editDraftDialog.value) {
|
||||
NewPostView(
|
||||
onClose = {
|
||||
editDraftDialog.value = false
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
draft = note,
|
||||
nav = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderMainPopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel?,
|
||||
note: Note,
|
||||
onDismiss: () -> Unit,
|
||||
showBlockAlertDialog: MutableState<Boolean>,
|
||||
showDeleteAlertDialog: MutableState<Boolean>,
|
||||
showReportDialog: MutableState<Boolean>,
|
||||
editDraftDialog: MutableState<Boolean>,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
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) {
|
||||
VerticalDivider(color = primaryLight)
|
||||
|
||||
@ -389,14 +426,20 @@ fun NoteQuickActionItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.size(70.dp).clickable { onClick() },
|
||||
modifier =
|
||||
Modifier
|
||||
.size(70.dp)
|
||||
.clickable { onClick() },
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp).padding(bottom = 5.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(24.dp)
|
||||
.padding(bottom = 5.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
|
||||
@ -527,7 +570,10 @@ fun QuickActionAlertDialog(
|
||||
text = { Text(textContent) },
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextButton(onClick = onClickDontShowAgain) {
|
||||
|
@ -310,7 +310,12 @@ fun RenderZapRaiser(
|
||||
}
|
||||
|
||||
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,
|
||||
progress = { zapraiserStatus.progress },
|
||||
)
|
||||
@ -590,6 +595,13 @@ fun ReplyReaction(
|
||||
IconButton(
|
||||
modifier = iconSizeModifier,
|
||||
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()) {
|
||||
onPress()
|
||||
} else {
|
||||
@ -776,7 +788,8 @@ fun LikeReaction(
|
||||
Box(
|
||||
contentAlignment = Center,
|
||||
modifier =
|
||||
Modifier.size(iconSize)
|
||||
Modifier
|
||||
.size(iconSize)
|
||||
.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
@ -784,6 +797,7 @@ fun LikeReaction(
|
||||
onClick = {
|
||||
likeClick(
|
||||
accountViewModel,
|
||||
baseNote,
|
||||
onMultipleChoices = { wantsToReact = true },
|
||||
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
|
||||
)
|
||||
@ -886,9 +900,17 @@ fun ObserveLikeText(
|
||||
|
||||
private fun likeClick(
|
||||
accountViewModel: AccountViewModel,
|
||||
baseNote: Note,
|
||||
onMultipleChoices: () -> 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()) {
|
||||
accountViewModel.toast(
|
||||
R.string.no_reactions_setup,
|
||||
@ -1082,6 +1104,14 @@ fun zapClick(
|
||||
onError: (String, String) -> 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()) {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
|
@ -52,6 +52,7 @@ fun WatchNoteEvent(
|
||||
LongPressToQuickAction(
|
||||
baseNote = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = null,
|
||||
) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
|
@ -179,7 +179,7 @@ fun ZapCustomDialog(
|
||||
)
|
||||
|
||||
ZapButton(
|
||||
isActive = postViewModel.canSend(),
|
||||
isActive = postViewModel.canSend() && !baseNote.isDraft(),
|
||||
) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
|
@ -185,6 +185,7 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
root = null,
|
||||
directMentions = setOf(),
|
||||
forkedFrom = null,
|
||||
draftTag = null,
|
||||
)
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
|
@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
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.note.VerticalDotsIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||
@ -122,6 +123,11 @@ fun NoteDropDownMenu(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val wantsToEditDraft =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (wantsToEditPost.value) {
|
||||
// avoids changing while drafting a note and a new event shows up.
|
||||
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(
|
||||
expanded = popupExpanded.value,
|
||||
onDismissRequest = onDismiss,
|
||||
@ -219,7 +237,15 @@ fun NoteDropDownMenu(
|
||||
},
|
||||
)
|
||||
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) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.edit_post)) },
|
||||
|
@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
fun RefreshingChatroomFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
@ -59,6 +61,7 @@ fun RefreshingChatroomFeedView(
|
||||
RenderChatroomFeedView(
|
||||
viewModel,
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
listState,
|
||||
nav,
|
||||
routeForLastRead,
|
||||
@ -72,6 +75,7 @@ fun RefreshingChatroomFeedView(
|
||||
fun RenderChatroomFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel,
|
||||
listState: LazyListState,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
@ -91,6 +95,7 @@ fun RenderChatroomFeedView(
|
||||
ChatroomFeedLoaded(
|
||||
state,
|
||||
accountViewModel,
|
||||
newPostViewModel,
|
||||
listState,
|
||||
nav,
|
||||
routeForLastRead,
|
||||
@ -108,6 +113,7 @@ fun RenderChatroomFeedView(
|
||||
fun ChatroomFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostViewModel: NewPostViewModel,
|
||||
listState: LazyListState,
|
||||
nav: (String) -> Unit,
|
||||
routeForLastRead: String,
|
||||
@ -130,6 +136,7 @@ fun ChatroomFeedLoaded(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostViewModel,
|
||||
nav = nav,
|
||||
onWantsToReply = onWantsToReply,
|
||||
)
|
||||
|
@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
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.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
@ -446,6 +447,10 @@ fun NoteMaster(
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
if (note.isDraft()) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
DisplayOtsIfInOriginal(note, editState, accountViewModel)
|
||||
}
|
||||
}
|
||||
@ -605,7 +610,7 @@ fun NoteMaster(
|
||||
ReactionsRow(note, true, editState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1209,6 +1209,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
baseNote: Note,
|
||||
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 (hasBoosted(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)
|
||||
|
||||
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
|
||||
|
@ -163,12 +163,17 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
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.withContext
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
@ -187,6 +192,7 @@ fun ChannelScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun PrepareChannelViewModels(
|
||||
baseChannel: Channel,
|
||||
@ -204,8 +210,21 @@ fun PrepareChannelViewModels(
|
||||
)
|
||||
|
||||
val channelScreenModel: NewPostViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
channelScreenModel.draftTextChanges
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
channelScreenModel.sendPost(localDraft = channelScreenModel.draftTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channelScreenModel.accountViewModel = accountViewModel
|
||||
channelScreenModel.account = accountViewModel.account
|
||||
channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex)
|
||||
|
||||
ChannelScreen(
|
||||
channel = baseChannel,
|
||||
@ -287,6 +306,7 @@ fun ChannelScreen(
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.idHex}",
|
||||
onWantsToReply = { replyTo.value = it },
|
||||
@ -295,7 +315,7 @@ fun ChannelScreen(
|
||||
|
||||
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()
|
||||
|
||||
@ -323,6 +343,7 @@ fun ChannelScreen(
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = null,
|
||||
)
|
||||
} else if (channel is LiveActivitiesChannel) {
|
||||
accountViewModel.account.sendLiveMessage(
|
||||
@ -332,10 +353,13 @@ fun ChannelScreen(
|
||||
mentions = tagger.pTags,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = null,
|
||||
)
|
||||
}
|
||||
newPostModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
}
|
||||
@ -346,6 +370,7 @@ fun ChannelScreen(
|
||||
fun DisplayReplyingToNote(
|
||||
replyingNote: Note?,
|
||||
accountViewModel: AccountViewModel,
|
||||
newPostModel: NewPostViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
@ -364,6 +389,7 @@ fun DisplayReplyingToNote(
|
||||
null,
|
||||
innerQuote = true,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostModel,
|
||||
nav = nav,
|
||||
onWantsToReply = {},
|
||||
)
|
||||
@ -665,7 +691,12 @@ fun ShowVideoStreaming(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
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 =
|
||||
remember(streamingInfo) {
|
||||
|
@ -116,14 +116,21 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
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.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.findURLs
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
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.withContext
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChatroomScreen(
|
||||
@ -206,6 +213,7 @@ fun LoadRoomByAuthor(
|
||||
content(room)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun PrepareChatroomViewModels(
|
||||
room: ChatroomKey,
|
||||
@ -230,8 +238,20 @@ fun PrepareChatroomViewModels(
|
||||
if (newPostModel.requiresNIP24) {
|
||||
newPostModel.nip24 = true
|
||||
}
|
||||
room.users.forEach {
|
||||
newPostModel.toUsers = TextFieldValue(newPostModel.toUsers.text + " @${Hex.decode(it).toNpub()}")
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = newPostModel) {
|
||||
launch(Dispatchers.IO) {
|
||||
newPostModel.draftTextChanges
|
||||
.receiveAsFlow()
|
||||
.debounce(1000)
|
||||
.collectLatest {
|
||||
newPostModel.sendPost(localDraft = newPostModel.draftTag)
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val hasNIP24 =
|
||||
accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any {
|
||||
@ -313,21 +333,28 @@ fun ChatroomScreen(
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
newPostViewModel = newPostModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Room/${room.hashCode()}",
|
||||
onWantsToReply = { replyTo.value = it },
|
||||
onWantsToReply = {
|
||||
replyTo.value = it
|
||||
newPostModel.originalNote = it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// LAST ROW
|
||||
PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
|
||||
val urls = findURLs(newPostModel.message.text)
|
||||
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
|
||||
|
||||
@ -339,6 +366,7 @@ fun ChatroomScreen(
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = null,
|
||||
)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
@ -348,6 +376,7 @@ fun ChatroomScreen(
|
||||
mentions = null,
|
||||
wantsToMarkAsSensitive = false,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,7 @@ val Size40dp = 40.dp
|
||||
val Size55dp = 55.dp
|
||||
val Size75dp = 75.dp
|
||||
|
||||
val HalfEndPadding = Modifier.padding(end = 5.dp)
|
||||
val HalfStartPadding = Modifier.padding(start = 5.dp)
|
||||
val StdStartPadding = Modifier.padding(start = 10.dp)
|
||||
val StdTopPadding = Modifier.padding(top = 10.dp)
|
||||
|
@ -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_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="route">Route</string>
|
||||
<string name="route_home">Home</string>
|
||||
@ -819,4 +821,9 @@
|
||||
<string name="accessibility_play_username">Play username as audio</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="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>
|
||||
|
@ -60,6 +60,7 @@ class ChannelMessageEvent(
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChannelMessageEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
@ -87,7 +88,7 @@ class ChannelMessageEvent(
|
||||
arrayOf("alt", ALT),
|
||||
)
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ class ChatMessageEvent(
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (ChatMessageEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
@ -106,7 +107,7 @@ class ChatMessageEvent(
|
||||
}
|
||||
// tags.add(arrayOf("alt", alt))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ class ClassifiedsEvent(
|
||||
nip94attachments: List<Event>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (ClassifiedsEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
@ -192,7 +193,7 @@ class ClassifiedsEvent(
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -79,6 +79,7 @@ class EventFactory {
|
||||
CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ContactListEvent.KIND -> ContactListEvent(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)
|
||||
EmojiPackSelectionEvent.KIND ->
|
||||
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
@ -94,6 +94,7 @@ class GitReplyEvent(
|
||||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (GitReplyEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
@ -156,7 +157,7 @@ class GitReplyEvent(
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (LiveActivitiesChatMessageEvent) -> Unit,
|
||||
) {
|
||||
val content = message
|
||||
@ -98,7 +99,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.events
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@ -78,6 +77,7 @@ class NIP24Factory {
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String? = null,
|
||||
onReady: (Result) -> Unit,
|
||||
) {
|
||||
val senderPublicKey = signer.pubKey
|
||||
@ -93,15 +93,25 @@ class NIP24Factory {
|
||||
markAsSensitive = markAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
isDraft = draftTag != null,
|
||||
nip94attachments = nip94attachments,
|
||||
) { senderMessage ->
|
||||
createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps ->
|
||||
if (draftTag != null) {
|
||||
onReady(
|
||||
Result(
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ class PollNoteEvent(
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PollNoteEvent) -> Unit,
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
@ -112,7 +113,7 @@ class PollNoteEvent(
|
||||
}
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ class PrivateDmEvent(
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
isDraft: Boolean,
|
||||
onReady: (PrivateDmEvent) -> Unit,
|
||||
) {
|
||||
var message = msg
|
||||
@ -167,7 +168,7 @@ class PrivateDmEvent(
|
||||
tags.add(arrayOf("alt", ALT))
|
||||
|
||||
signer.nip04Encrypt(message, recipientPubKey) { content ->
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ class TextNoteEvent(
|
||||
forkedFrom: Event? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
isDraft: Boolean,
|
||||
onReady: (TextNoteEvent) -> Unit,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +171,10 @@ class ExternalSignerLauncher(
|
||||
"sign_event",
|
||||
22242,
|
||||
),
|
||||
Permission(
|
||||
"sign_event",
|
||||
31234,
|
||||
),
|
||||
Permission(
|
||||
"nip04_encrypt",
|
||||
),
|
||||
|
@ -32,6 +32,7 @@ abstract class NostrSigner(val pubKey: HexKey) {
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
isDraft: Boolean = false,
|
||||
)
|
||||
|
||||
abstract fun nip04Encrypt(
|
||||
|
@ -40,7 +40,13 @@ class NostrSignerExternal(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
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 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(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
|
@ -38,9 +38,15 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
isDraft: Boolean,
|
||||
) {
|
||||
if (keyPair.privKey == null) return
|
||||
|
||||
if (isDraft) {
|
||||
unsignedEvent(createdAt, kind, tags, content, onReady)
|
||||
return
|
||||
}
|
||||
|
||||
if (isUnsignedPrivateEvent(kind, tags)) {
|
||||
// this is a private zap
|
||||
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(
|
||||
decryptedContent: String,
|
||||
toPublicKey: HexKey,
|
||||
|
Loading…
x
Reference in New Issue
Block a user