- Adds a Draft Screen

- Migrating drafts to new architecture where the Draft Event is sent to the screen instead of the inner event.
- Fixes lots of deletion and indexing bugs
This commit is contained in:
Vitor Pamplona 2024-03-29 17:38:31 -04:00
parent cd84c07fcc
commit 6e1418cd54
38 changed files with 1382 additions and 682 deletions

View File

@ -61,6 +61,7 @@ import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileServersEvent
import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageEvent
@ -845,18 +846,11 @@ class Account(
} }
} }
suspend fun delete(note: Note) { fun delete(note: Note) {
if (note.isDraft()) { delete(listOf(note))
note.event?.let {
val drafts = LocalCache.getDrafts(it.id())
return delete(drafts)
}
} else {
return delete(listOf(note))
}
} }
suspend fun delete(notes: List<Note>) { fun delete(notes: List<Note>) {
if (!isWriteable()) return if (!isWriteable()) return
val myEvents = notes.filter { it.author == userProfile() } val myEvents = notes.filter { it.author == userProfile() }
@ -906,12 +900,6 @@ class Account(
fun broadcast(note: Note) { fun broadcast(note: Note) {
note.event?.let { note.event?.let {
if (note.isDraft()) {
val drafts = LocalCache.getDrafts(it.id())
drafts.forEach { draftNote ->
broadcast(draftNote)
}
} else {
if (it is WrappedEvent && it.host != null) { if (it is WrappedEvent && it.host != null) {
it.host?.let { hostEvent -> Client.send(hostEvent) } it.host?.let { hostEvent -> Client.send(hostEvent) }
} else { } else {
@ -919,7 +907,6 @@ class Account(
} }
} }
} }
}
suspend fun updateAttestations() { suspend fun updateAttestations() {
Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations") Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations")
@ -1366,11 +1353,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent -> if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList) Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
@ -1428,11 +1417,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList) Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
@ -1454,7 +1445,21 @@ class Account(
} }
} }
fun sendPost( fun deleteDraft(draftTag: String) {
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
LocalCache.getAddressableNoteIfExists(key)?.let {
val noteEvent = it.event
if (noteEvent is DraftEvent) {
noteEvent.createDeletedEvent(signer) {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
delete(it)
}
}
suspend fun sendPost(
message: String, message: String,
replyTo: List<Note>?, replyTo: List<Note>?,
mentions: List<User>?, mentions: List<User>?,
@ -1496,11 +1501,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList) Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
@ -1587,11 +1594,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList) Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
@ -1639,11 +1648,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent) Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it) Client.send(it)
@ -1684,11 +1695,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent) Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it) Client.send(it)
@ -1756,11 +1769,13 @@ class Account(
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
if (draftTag != null) { if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent -> if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
Client.send(draftEvent) Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
} }
} else { } else {
Client.send(it) Client.send(it)
@ -1802,11 +1817,13 @@ class Account(
signer = signer, signer = signer,
) { ) {
if (draftTag != null) { if (draftTag != null) {
DraftEvent.create(draftTag, it.msg, signer) { draftEvent -> if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it.msg, emptyList(), signer) { draftEvent ->
Client.send(draftEvent) Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it.msg, null) }
LocalCache.addDraft(draftTag, draftEvent.id(), it.msg.id())
} }
} else { } else {
broadcastPrivately(it) broadcastPrivately(it)
@ -2325,7 +2342,11 @@ class Account(
} }
fun cachedDecryptContent(note: Note): String? { fun cachedDecryptContent(note: Note): String? {
val event = note.event return cachedDecryptContent(note.event)
}
fun cachedDecryptContent(event: EventInterface?): String? {
if (event == null) return null
return if (event is PrivateDmEvent && isWriteable()) { return if (event is PrivateDmEvent && isWriteable()) {
event.cachedContentFor(signer) event.cachedContentFor(signer)

View File

@ -105,6 +105,7 @@ import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
import com.vitorpamplona.quartz.events.WrappedEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
@ -129,7 +130,6 @@ object LocalCache {
val users = LargeCache<HexKey, User>() val users = LargeCache<HexKey, User>()
val notes = LargeCache<HexKey, Note>() val notes = LargeCache<HexKey, Note>()
val addressables = LargeCache<String, AddressableNote>() val addressables = LargeCache<String, AddressableNote>()
val drafts = ConcurrentHashMap<String, MutableList<Drafts>>()
val channels = LargeCache<HexKey, Channel>() val channels = LargeCache<HexKey, Channel>()
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10) val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
@ -142,34 +142,6 @@ object LocalCache {
return null return null
} }
fun draftNotes(draftTag: String): List<Note> {
return drafts[draftTag]?.mapNotNull {
getNoteIfExists(it.mainId)
} ?: listOf()
}
fun getDrafts(eventId: String): List<Note> {
return drafts.filter {
it.value.any { it.eventId == eventId }
}.values.map {
it.mapNotNull {
checkGetOrCreateNote(it.mainId)
}
}.flatten()
}
fun addDraft(
key: String,
mainId: String,
draftId: String,
) {
val data = drafts[key] ?: mutableListOf()
if (data.none { it.mainId == mainId }) {
data.add(Drafts(mainId, draftId))
drafts[key] = data
}
}
fun getOrCreateUser(key: HexKey): User { fun getOrCreateUser(key: HexKey): User {
// checkNotInMainThread() // checkNotInMainThread()
require(isValidHex(key = key)) { "$key is not a valid hex" } require(isValidHex(key = key)) { "$key is not a valid hex" }
@ -379,7 +351,7 @@ object LocalCache {
return return
} }
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -462,13 +434,7 @@ object LocalCache {
return return
} }
val repository = event.repository()?.toTag() val replyTo = computeReplyTo(event)
val replyTo =
event
.tagsWithoutCitations()
.filter { it != repository }
.mapNotNull { checkGetOrCreateNote(it) }
// println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}") // println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}")
@ -506,7 +472,7 @@ object LocalCache {
return return
} }
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } val replyTo = computeReplyTo(event)
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -541,7 +507,7 @@ object LocalCache {
return return
} }
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } val replyTo = computeReplyTo(event)
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -550,6 +516,58 @@ object LocalCache {
} }
} }
fun computeReplyTo(event: Event): List<Note> {
return when (event) {
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
is LnZapEvent ->
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
(event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList())
is LnZapRequestEvent ->
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is BadgeProfilesEvent ->
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) }
is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
is RepostEvent ->
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is GenericRepostEvent ->
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) }
is ReactionEvent ->
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is ReportEvent ->
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is ChannelMessageEvent ->
event
.tagsWithoutCitations()
.filter { it != event.channel() }
.mapNotNull { checkGetOrCreateNote(it) }
is LiveActivitiesChatMessageEvent ->
event
.tagsWithoutCitations()
.filter { it != event.activity()?.toTag() }
.mapNotNull { checkGetOrCreateNote(it) }
is DraftEvent -> {
event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { checkGetOrCreateAddressableNote(it.toTag()) }
}
else -> emptyList<Note>()
}
}
fun consume( fun consume(
event: PollNoteEvent, event: PollNoteEvent,
relay: Relay? = null, relay: Relay? = null,
@ -570,7 +588,7 @@ object LocalCache {
return return
} }
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -791,9 +809,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
val replyTo = val replyTo = computeReplyTo(event)
event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -812,7 +828,7 @@ object LocalCache {
// ${formattedDateTime(event.createdAt)}") // ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) } val awardDefinition = computeReplyTo(event)
note.loadEvent(event, author, awardDefinition) note.loadEvent(event, author, awardDefinition)
@ -872,6 +888,8 @@ object LocalCache {
val note = getOrCreateAddressableNote(event.address()) val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val replyTos = computeReplyTo(event)
if (version.event == null) { if (version.event == null) {
version.loadEvent(event, author, emptyList()) version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note) version.moveAllReferencesTo(note)
@ -886,7 +904,7 @@ object LocalCache {
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList()) note.loadEvent(event, author, replyTos)
refreshObservers(note) refreshObservers(note)
} }
@ -923,7 +941,7 @@ object LocalCache {
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
val repliesTo = event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } val repliesTo = computeReplyTo(event)
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -947,6 +965,49 @@ object LocalCache {
// must be the same author // must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) { if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add // reverts the add
deleteNote(deleteNote)
deletedAtLeastOne = true
}
}
val addressList = event.deleteAddresses()
val addressSet = addressList.toSet()
addressList
.mapNotNull { getAddressableNoteIfExists(it.toTag()) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) < event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
addressables.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
}
notes.forEach { key, note ->
val noteEvent = note.event
if (noteEvent is AddressableEvent && noteEvent.address() in addressSet) {
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
deleteNote(note)
deletedAtLeastOne = true
}
}
}
if (deletedAtLeastOne) {
val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
refreshObservers(note)
}
}
private fun deleteNote(deleteNote: Note) {
val deletedEvent = deleteNote.event
val mentions = val mentions =
deleteNote.event deleteNote.event
?.tags() ?.tags()
@ -968,14 +1029,14 @@ object LocalCache {
deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) } deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) }
(deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let { (deletedEvent as? LiveActivitiesChatMessageEvent)?.activity()?.let {
getChannelIfExists(it.toTag())?.removeNote(deleteNote) getChannelIfExists(it.toTag())?.removeNote(deleteNote)
} }
if (deleteNote.event is PrivateDmEvent) { if (deletedEvent is PrivateDmEvent) {
val author = deleteNote.author val author = deleteNote.author
val recipient = val recipient =
(deleteNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()?.let { deletedEvent.verifiedRecipientPubKey()?.let {
checkGetOrCreateUser(it) checkGetOrCreateUser(it)
} }
@ -985,14 +1046,31 @@ object LocalCache {
} }
} }
if (deletedEvent is DraftEvent) {
deletedEvent.allCache().forEach {
it?.let {
deindexDraftAsRealEvent(deleteNote, it)
}
}
}
if (deletedEvent is WrappedEvent) {
deleteWraps(deletedEvent)
}
notes.remove(deleteNote.idHex) notes.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
} }
if (deletedAtLeastOne) { fun deleteWraps(event: WrappedEvent) {
// refreshObservers() event.host?.let {
// seal
getNoteIfExists(it.id)?.let {
val noteEvent = it.event
if (noteEvent is WrappedEvent) {
deleteWraps(noteEvent)
}
}
notes.remove(it.id)
} }
} }
@ -1006,9 +1084,7 @@ object LocalCache {
// ${formattedDateTime(event.createdAt)}") // ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val repliesTo = val repliesTo = computeReplyTo(event)
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1028,9 +1104,7 @@ object LocalCache {
// ${formattedDateTime(event.createdAt)}") // ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val repliesTo = val repliesTo = computeReplyTo(event)
event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1052,7 +1126,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val communities = event.communities() val communities = event.communities()
val eventsApproved = event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } val eventsApproved = computeReplyTo(event)
val repliesTo = communities.map { getOrCreateAddressableNote(it) } val repliesTo = communities.map { getOrCreateAddressableNote(it) }
@ -1071,9 +1145,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val repliesTo = val repliesTo = computeReplyTo(event)
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1101,9 +1173,7 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) } val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
val repliesTo = val repliesTo = computeReplyTo(event)
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1202,11 +1272,7 @@ object LocalCache {
return return
} }
val replyTo = val replyTo = computeReplyTo(event)
event
.tagsWithoutCitations()
.filter { it != event.channel() }
.mapNotNull { checkGetOrCreateNote(it) }
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -1245,11 +1311,7 @@ object LocalCache {
return return
} }
val replyTo = val replyTo = computeReplyTo(event)
event
.tagsWithoutCitations()
.filter { it != event.activity()?.toTag() }
.mapNotNull { checkGetOrCreateNote(it) }
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
@ -1279,15 +1341,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = val repliesTo = computeReplyTo(event)
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
(
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map {
getOrCreateAddressableNote(it)
}
?: emptySet<Note>()
)
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1308,9 +1362,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = val repliesTo = computeReplyTo(event)
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -1512,7 +1564,7 @@ object LocalCache {
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
val repliesTo = event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } val repliesTo = computeReplyTo(event)
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -2046,7 +2098,112 @@ object LocalCache {
event: DraftEvent, event: DraftEvent,
relay: Relay?, relay: Relay?,
) { ) {
if (!event.isDeleted()) {
consumeBaseReplaceable(event, relay) consumeBaseReplaceable(event, relay)
event.allCache().forEach {
it?.let {
indexDraftAsRealEvent(event, it)
}
}
}
}
fun indexDraftAsRealEvent(
draftWrap: DraftEvent,
draft: Event,
) {
val note = getOrCreateAddressableNote(draftWrap.address())
val author = getOrCreateUser(draftWrap.pubKey)
when (draft) {
is PrivateDmEvent -> {
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
author.addMessage(recipient, note)
recipient.addMessage(author, note)
}
}
is ChatMessageEvent -> {
val recipientsHex = draft.recipientsPubKey().plus(draftWrap.pubKey).toSet()
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
if (recipients.isNotEmpty()) {
recipients.forEach {
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
val authorGroup =
if (groupMinusRecipient.isEmpty()) {
// note to self
ChatroomKey(persistentSetOf(it.pubkeyHex))
} else {
ChatroomKey(groupMinusRecipient.toImmutableSet())
}
it.addMessage(authorGroup, note)
}
}
}
is ChannelMessageEvent -> {
draft.channel()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.let { channel ->
channel.addNote(note)
}
}
}
is TextNoteEvent -> {
val replyTo = computeReplyTo(draft)
val author = getOrCreateUser(draftWrap.pubKey)
note.loadEvent(draftWrap, author, replyTo)
replyTo.forEach { it.addReply(note) }
}
}
}
fun deindexDraftAsRealEvent(
draftWrap: Note,
draft: Event,
) {
val author = draftWrap.author ?: return
when (draft) {
is PrivateDmEvent -> {
draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient ->
author.removeMessage(recipient, draftWrap)
recipient.removeMessage(author, draftWrap)
}
}
is ChatMessageEvent -> {
val recipientsHex = draft.recipientsPubKey().plus(author.pubkeyHex).toSet()
val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet()
if (recipients.isNotEmpty()) {
recipients.forEach {
val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex)
val authorGroup =
if (groupMinusRecipient.isEmpty()) {
// note to self
ChatroomKey(persistentSetOf(it.pubkeyHex))
} else {
ChatroomKey(groupMinusRecipient.toImmutableSet())
}
it.removeMessage(authorGroup, draftWrap)
}
}
}
is ChannelMessageEvent -> {
draft.channel()?.let { channelId ->
checkGetOrCreateChannel(channelId)?.let { channel ->
channel.removeNote(draftWrap)
}
}
}
is TextNoteEvent -> {
val replyTo = computeReplyTo(draft)
replyTo.forEach { it.removeReply(draftWrap) }
}
}
} }
fun justConsume( fun justConsume(

View File

@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GenericRepostEvent
@ -97,6 +98,14 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
fun dTag(): String? { fun dTag(): String? {
return (event as? AddressableEvent)?.dTag() return (event as? AddressableEvent)?.dTag()
} }
override fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<ATag>,
): Boolean {
val thisEvent = event
return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id()))
}
} }
@Stable @Stable
@ -184,12 +193,7 @@ open class Note(val idHex: String) {
open fun createdAt() = event?.createdAt() open fun createdAt() = event?.createdAt()
fun isDraft(): Boolean { fun isDraft() = event is DraftEvent
event?.let {
return it.sig().isBlank()
}
return false
}
fun loadEvent( fun loadEvent(
event: Event, event: Event,
@ -935,6 +939,14 @@ open class Note(val idHex: String) {
createOrDestroyFlowSync(false) createOrDestroyFlowSync(false)
} }
} }
open fun wasOrShouldBeDeletedBy(
deletionEvents: Set<HexKey>,
deletionAddressables: Set<ATag>,
): Boolean {
val thisEvent = event
return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address()))
}
} }
@Stable @Stable

View File

@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
@ -78,7 +80,7 @@ class ThreadAssembler {
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return emptySet<Note>() val note = LocalCache.checkGetOrCreateNote(noteId) ?: return emptySet<Note>()
if (note.event != null) { if (note.event != null) {
val thread = mutableSetOf<Note>() val thread = OnlyLatestVersionSet()
val threadRoot = searchRoot(note, thread) ?: note val threadRoot = searchRoot(note, thread) ?: note
@ -87,7 +89,7 @@ class ThreadAssembler {
// did not added them. // did not added them.
note.replies.forEach { loadDown(it, thread) } note.replies.forEach { loadDown(it, thread) }
thread.toSet() thread
} else { } else {
setOf(note) setOf(note)
} }
@ -109,3 +111,87 @@ class ThreadAssembler {
} }
} }
} }
class OnlyLatestVersionSet : MutableSet<Note> {
val map = hashMapOf<ATag, Long>()
val set = hashSetOf<Note>()
override fun add(element: Note): Boolean {
val loadedCreatedAt = element.createdAt()
val noteEvent = element.event
return if (element is AddressableNote && loadedCreatedAt != null) {
innerAdd(element.address, element, loadedCreatedAt)
} else if (noteEvent is AddressableEvent && loadedCreatedAt != null) {
innerAdd(noteEvent.address(), element, loadedCreatedAt)
} else {
set.add(element)
}
}
private fun innerAdd(
address: ATag,
element: Note,
loadedCreatedAt: Long,
): Boolean {
val existing = map.get(address)
return if (existing == null) {
map.put(address, loadedCreatedAt)
set.add(element)
} else {
if (loadedCreatedAt > existing) {
map.put(address, loadedCreatedAt)
set.add(element)
} else {
false
}
}
}
override fun addAll(elements: Collection<Note>): Boolean {
return elements.map { add(it) }.any()
}
override val size: Int
get() = set.size
override fun clear() {
set.clear()
map.clear()
}
override fun isEmpty(): Boolean {
return set.isEmpty()
}
override fun containsAll(elements: Collection<Note>): Boolean {
return set.containsAll(elements)
}
override fun contains(element: Note): Boolean {
return set.contains(element)
}
override fun iterator(): MutableIterator<Note> {
return set.iterator()
}
override fun retainAll(elements: Collection<Note>): Boolean {
return set.retainAll(elements)
}
override fun removeAll(elements: Collection<Note>): Boolean {
return elements.map { remove(it) }.any()
}
override fun remove(element: Note): Boolean {
element.address()?.let {
map.remove(it)
}
(element.event as? AddressableEvent)?.address()?.let {
map.remove(it)
}
return set.remove(element)
}
}

View File

@ -277,6 +277,18 @@ class User(val pubkeyHex: String) {
} }
} }
fun removeMessage(
room: ChatroomKey,
msg: Note,
) {
checkNotInMainThread()
val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg)
liveSet?.innerMessages?.invalidateData()
}
}
fun addRelayBeingUsed( fun addRelayBeingUsed(
relay: Relay, relay: Relay,
eventTime: Long, eventTime: Long,

View File

@ -200,8 +200,6 @@ class Nip96Uploader(val account: Account?) {
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) } nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
println(server.apiUrl.removeSuffix("/") + "/$hash.$extension")
val request = val request =
requestBuilder requestBuilder
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")

View File

@ -145,7 +145,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(ReportEvent.KIND), kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
since = since =
latestEOSEs.users[account.userProfile()] latestEOSEs.users[account.userProfile()]
@ -230,16 +230,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
) )
} }
fun createDraftsFilter() =
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(DraftEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
),
)
fun createGiftWrapsToMeFilter() = fun createGiftWrapsToMeFilter() =
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
@ -277,20 +267,14 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
is DraftEvent -> { is DraftEvent -> {
// Avoid decrypting over and over again if the event already exist. // Avoid decrypting over and over again if the event already exist.
if (!event.isDeleted()) {
val note = LocalCache.getNoteIfExists(event.id) val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return if (note != null && relay.brief in note.relays) return
LocalCache.justConsume(event, relay) // decrypts
event.plainContent(account.signer) { event.cachedDraft(account.signer) {}
val tag =
event.tags().filter { it.size > 1 && it[0] == "d" }.map {
it[1]
}.firstOrNull()
LocalCache.justConsume(it, relay) LocalCache.justConsume(event, relay)
tag?.let { lTag ->
LocalCache.addDraft(lTag, event.id(), it.id())
}
} }
} }
@ -376,7 +360,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountSettingsFilter(), createAccountSettingsFilter(),
createAccountLastPostsListFilter(), createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(), createOtherAccountsBaseFilter(),
createDraftsFilter(),
) )
.ifEmpty { null } .ifEmpty { null }
} else { } else {

View File

@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.Subscription import com.vitorpamplona.amethyst.service.relays.Subscription
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -293,7 +294,13 @@ abstract class NostrDataSource(val debugName: String) {
eventId: String, eventId: String,
relay: Relay, relay: Relay,
) { ) {
LocalCache.getNoteIfExists(eventId)?.addRelay(relay) val note = LocalCache.getNoteIfExists(eventId)
val noteEvent = note?.event
if (noteEvent is AddressableEvent) {
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
} else {
note?.addRelay(relay)
}
} }
open fun markAsEOSE( open fun markAsEOSE(

View File

@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GitReplyEvent import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
@ -57,6 +58,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
} }
return groupByEOSEPresence(addressesToWatch).map { return groupByEOSEPresence(addressesToWatch).map {
listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
@ -78,8 +80,23 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
// Max amount of "replies" to download on a specific event. // Max amount of "replies" to download on a specific event.
limit = 1000, limit = 1000,
), ),
),
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds =
listOf(
DeletionEvent.KIND,
),
tags = mapOf("a" to it.mapNotNull { it.address()?.toTag() }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
),
),
) )
} }.flatten()
} }
private fun createAddressFilter(): List<TypedFilter>? { private fun createAddressFilter(): List<TypedFilter>? {
@ -147,6 +164,20 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
limit = 1000, limit = 1000,
), ),
), ),
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds =
listOf(
DeletionEvent.KIND,
),
tags = mapOf("e" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
limit = 10,
),
),
) )
}.flatten() }.flatten()
} }

View File

@ -34,11 +34,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun NewPollOption( fun NewPollOption(
@ -49,10 +46,7 @@ fun NewPollOption(
val deleteIcon: @Composable (() -> Unit) = { val deleteIcon: @Composable (() -> Unit) = {
IconButton( IconButton(
onClick = { onClick = {
pollViewModel.pollOptions.remove(optionIndex) pollViewModel.removePollOption(optionIndex)
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
}, },
) { ) {
Icon( Icon(
@ -66,10 +60,7 @@ fun NewPollOption(
modifier = Modifier.weight(1F), modifier = Modifier.weight(1F),
value = pollViewModel.pollOptions[optionIndex] ?: "", value = pollViewModel.pollOptions[optionIndex] ?: "",
onValueChange = { onValueChange = {
pollViewModel.pollOptions[optionIndex] = it pollViewModel.updatePollOption(optionIndex, it)
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
}, },
label = { label = {
Text( Text(

View File

@ -119,7 +119,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -177,7 +176,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -206,15 +204,18 @@ fun NewPostView(
var showRelaysDialog by remember { mutableStateOf(false) } var showRelaysDialog by remember { mutableStateOf(false) }
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
LaunchedEffect(Unit) { LaunchedEffect(key1 = postViewModel.draftTag) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
postViewModel.draftTextChanges postViewModel.draftTextChanges
.receiveAsFlow() .receiveAsFlow()
.debounce(1000) .debounce(1000)
.collectLatest { .collectLatest {
postViewModel.sendPost(relayList = relayList, localDraft = postViewModel.draftTag) postViewModel.sendDraft(relayList = relayList)
} }
} }
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft) postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft)
@ -366,7 +367,7 @@ fun NewPostView(
} }
} }
if (enableMessageInterface) { if (postViewModel.wantsDirectMessage) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
@ -596,10 +597,7 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
} }
MarkAsSensitive(postViewModel) { MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive postViewModel.toggleMarkAsSensitive()
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
} }
AddGeoHash(postViewModel) { AddGeoHash(postViewModel) {
@ -846,10 +844,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField( MyTextField(
value = postViewModel.title, value = postViewModel.title,
onValueChange = { onValueChange = {
postViewModel.title = it postViewModel.updateTitle(it)
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {
@ -886,16 +881,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = postViewModel.price, value = postViewModel.price,
onValueChange = { onValueChange = {
runCatching { postViewModel.updatePrice(it)
if (it.text.isEmpty()) {
postViewModel.price = TextFieldValue("")
} else if (it.text.toLongOrNull() != null) {
postViewModel.price = it
}
}
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
placeholder = { placeholder = {
Text( Text(
@ -961,10 +947,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
options = conditionOptions, options = conditionOptions,
onSelect = { onSelect = {
postViewModel.condition = conditionTypes[it].first postViewModel.updateCondition(conditionTypes[it].first)
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
modifier = modifier =
Modifier Modifier
@ -1030,10 +1013,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
?: "", ?: "",
options = categoryOptions, options = categoryOptions,
onSelect = { onSelect = {
postViewModel.category = TextFieldValue(categoryTypes[it].second) postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
modifier = modifier =
Modifier Modifier
@ -1070,10 +1050,7 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField( MyTextField(
value = postViewModel.locationText, value = postViewModel.locationText,
onValueChange = { onValueChange = {
postViewModel.locationText = it postViewModel.updateLocation(it)
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {

View File

@ -49,12 +49,15 @@ import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.Split import com.vitorpamplona.amethyst.ui.components.Split
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageEvent
@ -84,7 +87,8 @@ enum class UserSuggestionAnchor {
@Stable @Stable
open class NewPostViewModel() : ViewModel() { open class NewPostViewModel() : ViewModel() {
var draftTag: String = UUID.randomUUID().toString() var draftTag: String by mutableStateOf(UUID.randomUUID().toString())
var accountViewModel: AccountViewModel? = null var accountViewModel: AccountViewModel? = null
var account: Account? = null var account: Account? = null
var requiresNIP24: Boolean = false var requiresNIP24: Boolean = false
@ -192,8 +196,17 @@ open class NewPostViewModel() : ViewModel() {
this.accountViewModel = accountViewModel this.accountViewModel = accountViewModel
this.account = accountViewModel.account this.account = accountViewModel.account
if (draft != null) { val noteEvent = draft?.event
loadFromDraft(draft, accountViewModel) val noteAuthor = draft?.author
if (draft != null && noteEvent is DraftEvent && noteAuthor != null) {
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag()
if (oldTag != null) {
draftTag = oldTag
}
loadFromDraft(innerNote, accountViewModel)
}
} else { } else {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
@ -227,14 +240,6 @@ open class NewPostViewModel() : ViewModel() {
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null contentToAddUrl = null
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
quote?.let { quote?.let {
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
@ -313,16 +318,13 @@ open class NewPostViewModel() : ViewModel() {
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
Log.d("draft", draft.event!!.toJson()) Log.d("draft", draft.event!!.toJson())
val draftEvent = draft.event ?: return
draftTag = LocalCache.drafts.filter {
it.value.any { it.eventId == draft.event?.id() }
}.keys.firstOrNull() ?: draftTag
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
contentToAddUrl = null contentToAddUrl = null
val localfowardZapTo = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zap" } ?: listOf() val localfowardZapTo = draftEvent.tags().filter { it.size > 1 && it[0] == "zap" }
forwardZapTo = Split() forwardZapTo = Split()
localfowardZapTo.forEach { localfowardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1]) val user = LocalCache.getOrCreateUser(it[1])
@ -332,9 +334,9 @@ open class NewPostViewModel() : ViewModel() {
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty() wantsForwardZapTo = localfowardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draft.event?.tags()?.any { it.size > 1 && it[0] == "content-warning" } ?: false wantsToMarkAsSensitive = draftEvent.tags().any { it.size > 1 && it[0] == "content-warning" }
wantsToAddGeoHash = draft.event?.tags()?.any { it.size > 1 && it[0] == "g" } ?: false wantsToAddGeoHash = draftEvent.tags().any { it.size > 1 && it[0] == "g" }
val zapraiser = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zapraiser" } ?: listOf() val zapraiser = draftEvent.tags().filter { it.size > 1 && it[0] == "zapraiser" }
wantsZapraiser = zapraiser.isNotEmpty() wantsZapraiser = zapraiser.isNotEmpty()
zapRaiserAmount = null zapRaiserAmount = null
if (wantsZapraiser) { if (wantsZapraiser) {
@ -342,25 +344,34 @@ open class NewPostViewModel() : ViewModel() {
} }
eTags = eTags =
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }?.mapNotNull { draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }.mapNotNull {
val note = LocalCache.checkGetOrCreateNote(it[1]) val note = LocalCache.checkGetOrCreateNote(it[1])
note note
} }
if (draftEvent !is PrivateDmEvent && draftEvent !is ChatMessageEvent) {
pTags = pTags =
draft.event?.tags()?.filter { it.size > 1 && it[0] == "p" }?.map { draftEvent.tags().filter { it.size > 1 && it[0] == "p" }.map {
LocalCache.getOrCreateUser(it[1]) LocalCache.getOrCreateUser(it[1])
} }
}
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "fork" }?.forEach { draftEvent.tags().filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it.get(3) == "fork" }.forEach {
val note = LocalCache.checkGetOrCreateNote(it[1]) val note = LocalCache.checkGetOrCreateNote(it[1])
forkedFromNote = note forkedFromNote = note
} }
originalNote = originalNote =
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }?.map { draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" }.map {
LocalCache.checkGetOrCreateNote(it[1]) LocalCache.checkGetOrCreateNote(it[1])
}?.firstOrNull() }.firstOrNull()
if (originalNote == null) {
originalNote =
draftEvent.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 canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
@ -368,14 +379,14 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = true wantsForwardZapTo = true
} }
val polls = draft.event?.tags()?.filter { it.size > 1 && it[0] == "poll_option" } ?: emptyList() val polls = draftEvent.tags().filter { it.size > 1 && it[0] == "poll_option" }
wantsPoll = polls.isNotEmpty() wantsPoll = polls.isNotEmpty()
polls.forEach { polls.forEach {
pollOptions[it[1].toInt()] = it[2] pollOptions[it[1].toInt()] = it[2]
} }
val minMax = draft.event?.tags()?.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") } ?: listOf() val minMax = draftEvent.tags().filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") }
minMax.forEach { minMax.forEach {
if (it[0] == "value_maximum") { if (it[0] == "value_maximum") {
valueMaximum = it[1].toInt() valueMaximum = it[1].toInt()
@ -384,33 +395,56 @@ open class NewPostViewModel() : ViewModel() {
} }
} }
wantsProduct = draft.event?.kind() == 30402 wantsProduct = draftEvent.kind() == 30402
title = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "title" }?.map { it[1] }?.firstOrNull() ?: "") title = TextFieldValue(draftEvent.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() ?: "") price = TextFieldValue(draftEvent.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() ?: "") category = TextFieldValue(draftEvent.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() ?: "") locationText = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "location" }.map { it[1] }?.firstOrNull() ?: "")
condition = ClassifiedsEvent.CONDITION.entries.firstOrNull { condition = ClassifiedsEvent.CONDITION.entries.firstOrNull {
it.value == draft.event?.tags()?.filter { it.size > 1 && it[0] == "condition" }?.map { it[1] }?.firstOrNull() it.value == draftEvent.tags().filter { it.size > 1 && it[0] == "condition" }.map { it[1] }.firstOrNull()
} ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW } ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW
message = wantsDirectMessage = draftEvent is PrivateDmEvent || draftEvent is ChatMessageEvent
if (draft.event is PrivateDmEvent) {
val event = draft.event as PrivateDmEvent draftEvent.subject()?.let {
TextFieldValue(event.cachedContentFor(accountViewModel.account.signer) ?: "") subject = TextFieldValue()
} else { }
TextFieldValue(draft.event?.content() ?: "")
message =
if (draftEvent is PrivateDmEvent) {
val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
toUsers = TextFieldValue("@$recepientNpub")
TextFieldValue(draftEvent.cachedContentFor(accountViewModel.account.signer) ?: "")
} else {
TextFieldValue(draftEvent.content())
}
requiresNIP24 = draftEvent is ChatMessageEvent
nip24 = draftEvent is ChatMessageEvent
if (draftEvent is ChatMessageEvent) {
toUsers =
TextFieldValue(
draftEvent.recipientsPubKey().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
)
} }
nip24 = draft.event is ChatMessageEvent
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
} }
fun sendPost( fun sendPost(relayList: List<Relay>? = null) {
relayList: List<Relay>? = null, viewModelScope.launch(Dispatchers.IO) {
localDraft: String? = null, innerSendPost(relayList, null)
) { accountViewModel?.deleteDraft(draftTag)
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList, localDraft) } cancel()
}
}
fun sendDraft(relayList: List<Relay>? = null) {
viewModelScope.launch(Dispatchers.IO) {
innerSendPost(relayList, draftTag)
}
} }
private suspend fun innerSendPost( private suspend fun innerSendPost(
@ -422,8 +456,7 @@ open class NewPostViewModel() : ViewModel() {
return return
} }
val tagger = val tagger = NewMessageTagger(message.text, pTags, eTags, originalNote?.channelHex(), accountViewModel!!)
NewMessageTagger(message.text, pTags, eTags, originalNote?.channelHex(), accountViewModel!!)
tagger.run() tagger.run()
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!) val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
@ -526,6 +559,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft,
) )
} else if (!dmUsers.isNullOrEmpty()) { } else if (!dmUsers.isNullOrEmpty()) {
if (nip24 || dmUsers.size > 1) { if (nip24 || dmUsers.size > 1) {
@ -599,19 +633,19 @@ open class NewPostViewModel() : ViewModel() {
} else { } else {
if (wantsPoll) { if (wantsPoll) {
account?.sendPoll( account?.sendPoll(
tagger.message, message = tagger.message,
tagger.eTags, replyTo = tagger.eTags,
tagger.pTags, mentions = tagger.pTags,
pollOptions, pollOptions = pollOptions,
valueMaximum, valueMaximum = valueMaximum,
valueMinimum, valueMinimum = valueMinimum,
consensusThreshold, consensusThreshold = consensusThreshold,
closedAt, closedAt = closedAt,
zapReceiver, zapReceiver = zapReceiver,
wantsToMarkAsSensitive, wantsToMarkAsSensitive = wantsToMarkAsSensitive,
localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
relayList, relayList = relayList,
geoHash, geohash = geoHash,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = localDraft, draftTag = localDraft,
) )
@ -673,9 +707,6 @@ open class NewPostViewModel() : ViewModel() {
) )
} }
} }
if (localDraft == null) {
cancel()
}
} }
fun upload( fun upload(
@ -759,7 +790,6 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = null urlPreview = null
isUploadingImage = false isUploadingImage = false
pTags = null pTags = null
eTags = null
wantsDirectMessage = false wantsDirectMessage = false
@ -777,6 +807,9 @@ open class NewPostViewModel() : ViewModel() {
wantsProduct = false wantsProduct = false
condition = ClassifiedsEvent.CONDITION.USED_LIKE_NEW condition = ClassifiedsEvent.CONDITION.USED_LIKE_NEW
locationText = TextFieldValue("")
title = TextFieldValue("")
category = TextFieldValue("")
price = TextFieldValue("") price = TextFieldValue("")
wantsForwardZapTo = false wantsForwardZapTo = false
@ -788,13 +821,16 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
userSuggestionAnchor = null userSuggestionAnchor = null
userSuggestionsMainMessage = null userSuggestionsMainMessage = null
originalNote = null
draftTag = UUID.randomUUID().toString()
NostrSearchEventOrUserDataSource.clear()
}
fun deleteDraft() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel?.deleteDraft(draftTag) accountViewModel?.deleteDraft(draftTag)
} }
NostrSearchEventOrUserDataSource.clear()
} }
open fun findUrlInMessage(): String? { open fun findUrlInMessage(): String? {
@ -809,8 +845,8 @@ open class NewPostViewModel() : ViewModel() {
pTags = pTags?.filter { it != userToRemove } pTags = pTags?.filter { it != userToRemove }
} }
open suspend fun saveDraft() { private fun saveDraft() {
draftTextChanges.send("") draftTextChanges.trySend("")
} }
open fun updateMessage(it: TextFieldValue) { open fun updateMessage(it: TextFieldValue) {
@ -836,10 +872,8 @@ open class NewPostViewModel() : ViewModel() {
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
open fun updateToUsers(it: TextFieldValue) { open fun updateToUsers(it: TextFieldValue) {
toUsers = it toUsers = it
@ -862,17 +896,13 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
open fun updateSubject(it: TextFieldValue) { open fun updateSubject(it: TextFieldValue) {
subject = it subject = it
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
open fun updateZapForwardTo(it: TextFieldValue) { open fun updateZapForwardTo(it: TextFieldValue) {
forwardZapToEditting = it forwardZapToEditting = it
@ -898,10 +928,8 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
open fun autocompleteWithUser(item: User) { open fun autocompleteWithUser(item: User) {
userSuggestionAnchor?.let { userSuggestionAnchor?.let {
@ -947,10 +975,8 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList() userSuggestions = emptyList()
} }
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> { private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
return mutableStateMapOf(Pair(0, ""), Pair(1, "")) return mutableStateMapOf(Pair(0, ""), Pair(1, ""))
@ -1020,10 +1046,8 @@ open class NewPostViewModel() : ViewModel() {
message = message.insertUrlAtCursor(imageUrl) message = message.insertUrlAtCursor(imageUrl)
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
}, },
onError = { onError = {
isUploadingImage = false isUploadingImage = false
@ -1067,10 +1091,8 @@ open class NewPostViewModel() : ViewModel() {
} }
urlPreview = findUrlInMessage() urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
}, },
onError = { onError = {
isUploadingImage = false isUploadingImage = false
@ -1090,7 +1112,7 @@ open class NewPostViewModel() : ViewModel() {
locUtil?.let { locUtil?.let {
location = location =
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() } it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
viewModelScope.launch(Dispatchers.IO) { saveDraft() } saveDraft()
} }
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() } viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
} }
@ -1116,11 +1138,9 @@ open class NewPostViewModel() : ViewModel() {
nip24 = !nip24 nip24 = !nip24
} }
if (message.text.isNotBlank()) { if (message.text.isNotBlank()) {
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
} }
}
fun updateMinZapAmountForPoll(textMin: String) { fun updateMinZapAmountForPoll(textMin: String) {
if (textMin.isNotEmpty()) { if (textMin.isNotEmpty()) {
@ -1139,10 +1159,8 @@ open class NewPostViewModel() : ViewModel() {
} }
checkMinMax() checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
fun updateMaxZapAmountForPoll(textMax: String) { fun updateMaxZapAmountForPoll(textMax: String) {
if (textMax.isNotEmpty()) { if (textMax.isNotEmpty()) {
@ -1161,10 +1179,8 @@ open class NewPostViewModel() : ViewModel() {
} }
checkMinMax() checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft() saveDraft()
} }
}
fun checkMinMax() { fun checkMinMax() {
if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) { if ((valueMinimum ?: 0) > (valueMaximum ?: Int.MAX_VALUE)) {
@ -1182,6 +1198,60 @@ open class NewPostViewModel() : ViewModel() {
) { ) {
forwardZapTo.updatePercentage(index, sliderValue) forwardZapTo.updatePercentage(index, sliderValue)
} }
fun updateZapRaiserAmount(newAmount: Long?) {
zapRaiserAmount = newAmount
saveDraft()
}
fun removePollOption(optionIndex: Int) {
pollOptions.remove(optionIndex)
saveDraft()
}
fun updatePollOption(
optionIndex: Int,
text: String,
) {
pollOptions[optionIndex] = text
saveDraft()
}
fun toggleMarkAsSensitive() {
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
saveDraft()
}
fun updateTitle(it: TextFieldValue) {
title = it
saveDraft()
}
fun updatePrice(it: TextFieldValue) {
runCatching {
if (it.text.isEmpty()) {
price = TextFieldValue("")
} else if (it.text.toLongOrNull() != null) {
price = it
}
}
saveDraft()
}
fun updateCondition(newCondition: ClassifiedsEvent.CONDITION) {
condition = newCondition
saveDraft()
}
fun updateCategory(value: TextFieldValue) {
category = value
saveDraft()
}
fun updateLocation(it: TextFieldValue) {
locationText = it
saveDraft()
}
} }
enum class GeohashPrecision(val digits: Int) { enum class GeohashPrecision(val digits: Int) {

View File

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

View File

@ -18,6 +18,39 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.ui.dal
data class Drafts(val mainId: String, val eventId: String) import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.DraftEvent
class DraftEventsFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return collection.filterTo(HashSet()) {
acceptableEvent(it)
}
}
override fun feed(): List<Note> {
val drafts =
LocalCache.addressables.filterIntoSet { _, note ->
acceptableEvent(note)
}
return sort(drafts)
}
fun acceptableEvent(it: Note): Boolean {
val noteEvent = it.event
return noteEvent is DraftEvent && noteEvent.pubKey == account.userProfile().pubkeyHex
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(DefaultFeedOrder)
}
}

View File

@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreenByAuthor import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreenByAuthor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.DiscoverScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DraftListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
@ -214,6 +215,7 @@ fun AppNavigation(
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) }) composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) }) composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) })
Route.Profile.let { route -> Route.Profile.let { route ->
composable( composable(

View File

@ -185,6 +185,8 @@ private fun RenderTopRouteBar(
Route.Discover.base -> DiscoveryTopBar(followLists, drawerState, accountViewModel, nav) Route.Discover.base -> DiscoveryTopBar(followLists, drawerState, accountViewModel, nav)
Route.Notification.base -> NotificationTopBar(followLists, drawerState, accountViewModel, nav) Route.Notification.base -> NotificationTopBar(followLists, drawerState, accountViewModel, nav)
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack) Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack)
Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack)
else -> { else -> {
if (id != null) { if (id != null) {
when (currentRoute) { when (currentRoute) {

View File

@ -88,7 +88,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.ClickableText
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@ -462,12 +461,6 @@ fun ListContent(
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) } val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
val context = LocalContext.current val context = LocalContext.current
var draftText by remember {
mutableStateOf<String?>(null)
}
var wantsToPost by remember { mutableStateOf(false) }
Column( Column(
modifier = modifier =
modifier modifier
@ -492,6 +485,15 @@ fun ListContent(
route = Route.Bookmarks.route, route = Route.Bookmarks.route,
) )
NavigationRow(
title = stringResource(R.string.drafts),
icon = Route.Drafts.icon,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
drawerState = drawerState,
route = Route.Drafts.route,
)
IconRowRelays( IconRowRelays(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onClick = { onClick = {
@ -588,18 +590,6 @@ fun ListContent(
) )
} }
if (wantsToPost) {
NewPostView(
{
wantsToPost = false
draftText = null
coroutineScope.launch { drawerState.close() }
},
accountViewModel = accountViewModel,
nav = nav,
)
}
if (disconnectTorDialog) { if (disconnectTorDialog) {
AlertDialog( AlertDialog(
title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) }, title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) },

View File

@ -148,6 +148,13 @@ sealed class Route(
contentDescriptor = R.string.route_home, contentDescriptor = R.string.route_home,
) )
object Drafts :
Route(
route = "Drafts",
icon = R.drawable.ic_topics,
contentDescriptor = R.string.drafts,
)
object Profile : object Profile :
Route( Route(
route = "User/{id}", route = "User/{id}",

View File

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

View File

@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.emptyLineItemModifier
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@ -77,6 +76,7 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.DraftEvent
@Composable @Composable
fun ChatroomHeaderCompose( fun ChatroomHeaderCompose(
@ -102,6 +102,17 @@ fun ChatroomComposeChannelOrUser(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
if (baseNote.event is DraftEvent) {
ObserveDraftEvent(baseNote, accountViewModel) {
val channelHex by remember(it) { derivedStateOf { it.channelHex() } }
if (channelHex != null) {
ChatroomChannel(channelHex!!, it, accountViewModel, nav)
} else {
ChatroomPrivateMessages(it, accountViewModel, nav)
}
}
} else {
val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } } val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } }
if (channelHex != null) { if (channelHex != null) {
@ -109,6 +120,7 @@ fun ChatroomComposeChannelOrUser(
} else { } else {
ChatroomPrivateMessages(baseNote, accountViewModel, nav) ChatroomPrivateMessages(baseNote, accountViewModel, nav)
} }
}
} }
@Composable @Composable
@ -128,9 +140,7 @@ private fun ChatroomPrivateMessages(
if (room != null) { if (room != null) {
UserRoomCompose(baseNote, room, accountViewModel, nav) UserRoomCompose(baseNote, room, accountViewModel, nav)
} else { } else {
Box(emptyLineItemModifier) { BlankNote()
// Makes sure just a max amount of objects are loaded.
}
} }
} }
} }

View File

@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
@ -91,6 +90,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
@ -103,7 +103,6 @@ fun ChatroomMessageCompose(
innerQuote: Boolean = false, innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@ -122,7 +121,6 @@ fun ChatroomMessageCompose(
canPreview, canPreview,
parentBackgroundColor, parentBackgroundColor,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
onWantsToReply, onWantsToReply,
) )
@ -139,7 +137,6 @@ fun NormalChatNote(
canPreview: Boolean = true, canPreview: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@ -259,7 +256,6 @@ fun NormalChatNote(
availableBubbleSize, availableBubbleSize,
showDetails, showDetails,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
) )
} }
@ -270,7 +266,6 @@ fun NormalChatNote(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false }, onDismiss = { popupExpanded = false },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
) )
} }
} }
@ -288,7 +283,6 @@ private fun RenderBubble(
availableBubbleSize: MutableState<Int>, availableBubbleSize: MutableState<Int>,
showDetails: State<Boolean>, showDetails: State<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val bubbleSize = remember { mutableIntStateOf(0) } val bubbleSize = remember { mutableIntStateOf(0) }
@ -318,7 +312,6 @@ private fun RenderBubble(
canPreview, canPreview,
showDetails, showDetails,
accountViewModel, accountViewModel,
newPostViewModel,
nav, nav,
) )
} }
@ -337,7 +330,6 @@ private fun MessageBubbleLines(
canPreview: Boolean, canPreview: Boolean,
showDetails: State<Boolean>, showDetails: State<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
if (drawAuthorInfo) { if (drawAuthorInfo) {
@ -349,20 +341,22 @@ private fun MessageBubbleLines(
) )
} }
if (baseNote.event !is DraftEvent) {
RenderReplyRow( RenderReplyRow(
note = baseNote, note = baseNote,
innerQuote = innerQuote, innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor, backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )
}
NoteRow( NoteRow(
note = baseNote, note = baseNote,
canPreview = canPreview, canPreview = canPreview,
innerQuote = innerQuote, innerQuote = innerQuote,
onWantsToReply = onWantsToReply,
backgroundBubbleColor = backgroundBubbleColor, backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@ -407,12 +401,11 @@ private fun RenderReplyRow(
innerQuote: Boolean, innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
if (!innerQuote && note.replyTo?.lastOrNull() != null) { if (!innerQuote && note.replyTo?.lastOrNull() != null) {
RenderReply(note, backgroundBubbleColor, accountViewModel, newPostViewModel, nav, onWantsToReply) RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply)
} }
} }
@ -421,7 +414,6 @@ private fun RenderReply(
note: Note, note: Note,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit, nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
) { ) {
@ -440,7 +432,6 @@ private fun RenderReply(
innerQuote = true, innerQuote = true,
parentBackgroundColor = backgroundBubbleColor, parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )
@ -453,6 +444,7 @@ private fun NoteRow(
note: Note, note: Note,
canPreview: Boolean, canPreview: Boolean,
innerQuote: Boolean, innerQuote: Boolean,
onWantsToReply: (Note) -> Unit,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
@ -465,6 +457,17 @@ private fun NoteRow(
is ChannelMetadataEvent -> { is ChannelMetadataEvent -> {
RenderChangeChannelMetadataNote(note) RenderChangeChannelMetadataNote(note)
} }
is DraftEvent -> {
RenderDraftEvent(
note,
canPreview,
innerQuote,
onWantsToReply,
backgroundBubbleColor,
accountViewModel,
nav,
)
}
else -> { else -> {
RenderRegularTextNote( RenderRegularTextNote(
note, note,
@ -479,6 +482,38 @@ private fun NoteRow(
} }
} }
@Composable
private fun RenderDraftEvent(
note: Note,
canPreview: Boolean,
innerQuote: Boolean,
onWantsToReply: (Note) -> Unit,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
ObserveDraftEvent(note, accountViewModel) {
RenderReplyRow(
note = it,
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
onWantsToReply = onWantsToReply,
)
NoteRow(
note = it,
canPreview = canPreview,
innerQuote = innerQuote,
onWantsToReply = onWantsToReply,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable @Composable
private fun ConstrainedStatusRow( private fun ConstrainedStatusRow(
bubbleSize: MutableState<Int>, bubbleSize: MutableState<Int>,

View File

@ -40,6 +40,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -141,6 +142,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FhirResourceEvent
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
@ -244,7 +246,7 @@ fun AcceptableNote(
nav = nav, nav = nav,
) )
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
showPopup, showPopup,
-> ->
CheckNewAndRenderNote( CheckNewAndRenderNote(
@ -279,9 +281,7 @@ fun AcceptableNote(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
showPopup,
->
CheckNewAndRenderNote( CheckNewAndRenderNote(
baseNote = baseNote, baseNote = baseNote,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
@ -466,7 +466,7 @@ fun InnerNoteWithReactions(
} }
} }
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftEvent
if (isNotRepost) { if (isNotRepost) {
if (makeItShort) { if (makeItShort) {
@ -483,6 +483,10 @@ fun InnerNoteWithReactions(
nav = nav, nav = nav,
) )
} }
} else {
if (baseNote.event is DraftEvent) {
Spacer(modifier = DoubleVertSpacer)
}
} }
} }
@ -566,6 +570,7 @@ private fun RenderNoteRow(
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav) is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav) is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav)
is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav) is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav)
is DraftEvent -> RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
@ -682,6 +687,68 @@ private fun RenderNoteRow(
} }
} }
@Composable
fun ObserveDraftEvent(
note: Note,
accountViewModel: AccountViewModel,
render: @Composable (Note) -> Unit,
) {
val noteState by note.live().metadata.observeAsState()
val noteEvent = noteState?.note?.event as? DraftEvent ?: return
val noteAuthor = noteState?.note?.author ?: return
val innerNote =
produceState(initialValue = accountViewModel.createTempCachedDraftNote(noteEvent, noteAuthor), noteEvent.id) {
if (value == null || value?.event?.id() != noteEvent.id) {
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) {
value = it
}
}
}
innerNote.value?.let {
render(it)
}
}
@Composable
fun RenderDraft(
note: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
ObserveDraftEvent(note, accountViewModel) {
val edits = remember { mutableStateOf(GenericLoadable.Empty<EditState>()) }
ReplyRow(
it,
true,
backgroundColor,
accountViewModel,
nav,
)
RenderNoteRow(
baseNote = it,
backgroundColor = backgroundColor,
makeItShort = false,
canPreview = true,
editState = edits,
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
)
val zapSplits = remember(it.event) { it.event?.hasZapSplitSetup() }
if (zapSplits == true) {
Spacer(modifier = HalfDoubleVertSpacer)
DisplayZapSplits(it.event!!, false, accountViewModel, nav)
}
}
}
@Composable @Composable
fun RenderRepost( fun RenderRepost(
note: Note, note: Note,

View File

@ -40,7 +40,6 @@ import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FormatQuote import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonRemove import androidx.compose.material.icons.filled.PersonRemove
@ -86,7 +85,6 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
@ -135,7 +133,6 @@ val externalLinkForNote = { note: Note ->
fun LongPressToQuickAction( fun LongPressToQuickAction(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
content: @Composable (() -> Unit) -> Unit, content: @Composable (() -> Unit) -> Unit,
) { ) {
val popupExpanded = remember { mutableStateOf(false) } val popupExpanded = remember { mutableStateOf(false) }
@ -144,7 +141,7 @@ fun LongPressToQuickAction(
content(showPopup) content(showPopup)
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel, newPostViewModel) NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel)
} }
@Composable @Composable
@ -153,7 +150,6 @@ fun NoteQuickActionMenu(
popupExpanded: Boolean, popupExpanded: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
) { ) {
val showSelectTextDialog = remember { mutableStateOf(false) } val showSelectTextDialog = remember { mutableStateOf(false) }
val showDeleteAlertDialog = remember { mutableStateOf(false) } val showDeleteAlertDialog = remember { mutableStateOf(false) }
@ -164,7 +160,6 @@ fun NoteQuickActionMenu(
if (popupExpanded) { if (popupExpanded) {
RenderMainPopup( RenderMainPopup(
accountViewModel, accountViewModel,
newPostViewModel,
note, note,
onDismiss, onDismiss,
showBlockAlertDialog, showBlockAlertDialog,
@ -223,7 +218,6 @@ fun NoteQuickActionMenu(
@Composable @Composable
private fun RenderMainPopup( private fun RenderMainPopup(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
note: Note, note: Note,
onDismiss: () -> Unit, onDismiss: () -> Unit,
showBlockAlertDialog: MutableState<Boolean>, showBlockAlertDialog: MutableState<Boolean>,
@ -300,6 +294,7 @@ private fun RenderMainPopup(
} }
} }
/*
if (note.isDraft()) { if (note.isDraft()) {
VerticalDivider(color = primaryLight) VerticalDivider(color = primaryLight)
NoteQuickActionItem( NoteQuickActionItem(
@ -314,7 +309,7 @@ private fun RenderMainPopup(
onDismiss() onDismiss()
} }
} }
} }*/
if (!isOwnNote) { if (!isOwnNote) {
VerticalDivider(color = primaryLight) VerticalDivider(color = primaryLight)

View File

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

View File

@ -57,6 +57,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@ -175,6 +176,7 @@ class AddBountyAmountViewModel : ViewModel() {
val newValue = nextAmount.text.trim().toLongOrNull() val newValue = nextAmount.text.trim().toLongOrNull()
if (newValue != null) { if (newValue != null) {
viewModelScope.launch {
account?.sendPost( account?.sendPost(
message = newValue.toString(), message = newValue.toString(),
replyTo = listOfNotNull(bounty), replyTo = listOfNotNull(bounty),
@ -191,6 +193,7 @@ class AddBountyAmountViewModel : ViewModel() {
nextAmount = TextFieldValue("") nextAmount = TextFieldValue("")
} }
} }
}
fun cancel() { fun cancel() {
nextAmount = TextFieldValue("") nextAmount = TextFieldValue("")
@ -237,10 +240,8 @@ fun AddBountyAmountDialog(
PostButton( PostButton(
onPost = { onPost = {
scope.launch(Dispatchers.IO) {
postViewModel.sendPost() postViewModel.sendPost()
onClose() onClose()
}
}, },
isActive = postViewModel.hasChanged(), isActive = postViewModel.hasChanged(),
) )

View File

@ -237,7 +237,7 @@ fun NoteDropDownMenu(
}, },
) )
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
if (note.isDraft()) { if (state.isLoggedUser && note.isDraft()) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.edit_draft)) }, text = { Text(stringResource(R.string.edit_draft)) },
onClick = { onClick = {

View File

@ -38,21 +38,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.quartz.events.DraftEvent
@Composable @Composable
fun RefreshingChatroomFeedView( fun RefreshingChatroomFeedView(
viewModel: FeedViewModel, viewModel: FeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
avoidDraft: String? = null,
scrollStateKey: String? = null, scrollStateKey: String? = null,
enablePullRefresh: Boolean = true, enablePullRefresh: Boolean = true,
) { ) {
@ -61,11 +61,11 @@ fun RefreshingChatroomFeedView(
RenderChatroomFeedView( RenderChatroomFeedView(
viewModel, viewModel,
accountViewModel, accountViewModel,
newPostViewModel,
listState, listState,
nav, nav,
routeForLastRead, routeForLastRead,
onWantsToReply, onWantsToReply,
avoidDraft,
) )
} }
} }
@ -75,11 +75,11 @@ fun RefreshingChatroomFeedView(
fun RenderChatroomFeedView( fun RenderChatroomFeedView(
viewModel: FeedViewModel, viewModel: FeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
avoidDraft: String? = null,
) { ) {
val feedState by viewModel.feedContent.collectAsStateWithLifecycle() val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
@ -95,11 +95,11 @@ fun RenderChatroomFeedView(
ChatroomFeedLoaded( ChatroomFeedLoaded(
state, state,
accountViewModel, accountViewModel,
newPostViewModel,
listState, listState,
nav, nav,
routeForLastRead, routeForLastRead,
onWantsToReply, onWantsToReply,
avoidDraft,
) )
} }
is FeedState.Loading -> { is FeedState.Loading -> {
@ -113,11 +113,11 @@ fun RenderChatroomFeedView(
fun ChatroomFeedLoaded( fun ChatroomFeedLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState, listState: LazyListState,
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String, routeForLastRead: String,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
avoidDraft: String? = null,
) { ) {
LaunchedEffect(state.feed.value.firstOrNull()) { LaunchedEffect(state.feed.value.firstOrNull()) {
if (listState.firstVisibleItemIndex <= 1) { if (listState.firstVisibleItemIndex <= 1) {
@ -132,14 +132,16 @@ fun ChatroomFeedLoaded(
state = listState, state = listState,
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val noteEvent = item.event
if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() != avoidDraft) {
ChatroomMessageCompose( ChatroomMessageCompose(
baseNote = item, baseNote = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
) )
}
NewSubject(item) NewSubject(item)
} }
} }

View File

@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
@ -60,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.DeletionEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -267,6 +269,16 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) :
} }
} }
@Stable
class NostrDraftEventsFeedViewModel(val account: Account) :
FeedViewModel(DraftEventsFeedFilter(account)) {
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrDraftEventsFeedViewModel : ViewModel> create(modelClass: Class<NostrDraftEventsFeedViewModel>): NostrDraftEventsFeedViewModel {
return NostrDraftEventsFeedViewModel(account) as NostrDraftEventsFeedViewModel
}
}
}
class NostrUserAppRecommendationsFeedViewModel(val user: User) : class NostrUserAppRecommendationsFeedViewModel(val user: User) :
FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
class Factory(val user: User) : ViewModelProvider.Factory { class Factory(val user: User) : ViewModelProvider.Factory {
@ -344,9 +356,24 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) :
val oldNotesState = _feedContent.value val oldNotesState = _feedContent.value
if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) { if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) {
if (oldNotesState is FeedState.Loaded) { if (oldNotesState is FeedState.Loaded) {
val deletionEvents: List<DeletionEvent> =
newItems.mapNotNull {
val noteEvent = it.event
if (noteEvent is DeletionEvent) noteEvent else null
}
val oldList =
if (deletionEvents.isEmpty()) {
oldNotesState.feed.value
} else {
val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEvents() }
val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() }
oldNotesState.feed.value.filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) }.toImmutableList()
}
val newList = val newList =
localFilter localFilter
.updateListWith(oldNotesState.feed.value, newItems) .updateListWith(oldList, newItems)
.distinctBy { it.idHex } .distinctBy { it.idHex }
.toImmutableList() .toImmutableList()
if (!equalImmutableLists(newList, oldNotesState.feed.value)) { if (!equalImmutableLists(newList, oldNotesState.feed.value)) {

View File

@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.note.RenderDraft
import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus
@ -155,6 +156,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FhirResourceEvent
@ -540,6 +542,8 @@ fun NoteMaster(
RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav) RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AppDefinitionEvent) { } else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav) RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is DraftEvent) {
RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) { } else if (noteEvent is HighlightEvent) {
DisplayHighlight( DisplayHighlight(
noteEvent.quote(), noteEvent.quote(),
@ -610,7 +614,7 @@ fun NoteMaster(
ReactionsRow(note, true, editState, accountViewModel, nav) ReactionsRow(note, true, editState, accountViewModel, nav)
} }
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel, null) NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
} }
} }

View File

@ -71,8 +71,10 @@ import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GiftWrapEvent
@ -573,6 +575,10 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
return account.cachedDecryptContent(note) return account.cachedDecryptContent(note)
} }
fun cachedDecrypt(event: EventInterface?): String? {
return account.cachedDecryptContent(event)
}
fun decrypt( fun decrypt(
note: Note, note: Note,
onReady: (String) -> Unit, onReady: (String) -> Unit,
@ -1313,8 +1319,40 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
} }
suspend fun deleteDraft(draftTag: String) { suspend fun deleteDraft(draftTag: String) {
val notes = LocalCache.draftNotes(draftTag) account.deleteDraft(draftTag)
account.delete(notes) }
fun createTempCachedDraftNote(
noteEvent: DraftEvent,
author: User,
): Note? {
return noteEvent.preCachedDraft(account.signer)?.let { createTempDraftNote(it, author) }
}
fun createTempDraftNote(
noteEvent: DraftEvent,
author: User,
onReady: (Note) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
noteEvent.cachedDraft(account.signer) {
onReady(createTempDraftNote(it, author))
}
}
}
fun createTempDraftNote(
innerEvent: Event,
author: User,
): Note {
val note =
if (innerEvent is AddressableEvent) {
AddressableNote(innerEvent.address())
} else {
Note(innerEvent.id)
}
note.loadEvent(innerEvent, author, LocalCache.computeReplyTo(innerEvent))
return note
} }
val bechLinkCache = CachedLoadedBechLink(this) val bechLinkCache = CachedLoadedBechLink(this)

View File

@ -64,6 +64,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -210,21 +211,8 @@ fun PrepareChannelViewModels(
) )
val channelScreenModel: NewPostViewModel = viewModel() val channelScreenModel: NewPostViewModel = viewModel()
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
channelScreenModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
channelScreenModel.sendPost(localDraft = channelScreenModel.draftTag)
}
}
}
channelScreenModel.accountViewModel = accountViewModel channelScreenModel.accountViewModel = accountViewModel
channelScreenModel.account = accountViewModel.account channelScreenModel.account = accountViewModel.account
channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex)
ChannelScreen( ChannelScreen(
channel = baseChannel, channel = baseChannel,
@ -306,22 +294,51 @@ fun ChannelScreen(
RefreshingChatroomFeedView( RefreshingChatroomFeedView(
viewModel = feedViewModel, viewModel = feedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
routeForLastRead = "Channel/${channel.idHex}", routeForLastRead = "Channel/${channel.idHex}",
avoidDraft = newPostModel.draftTag,
onWantsToReply = { replyTo.value = it }, onWantsToReply = { replyTo.value = it },
) )
} }
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } } replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
newPostModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
innerSendPost(replyTo, channel, newPostModel, accountViewModel, newPostModel.draftTag)
}
}
}
// LAST ROW // LAST ROW
EditFieldRow(newPostModel, isPrivate = false, accountViewModel = accountViewModel) { EditFieldRow(newPostModel, isPrivate = false, accountViewModel = accountViewModel) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
innerSendPost(replyTo, channel, newPostModel, accountViewModel, null)
newPostModel.message = TextFieldValue("")
replyTo.value = null
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
feedViewModel.sendToTop()
}
}
}
}
private suspend fun innerSendPost(
replyTo: MutableState<Note?>,
channel: Channel,
newPostModel: NewPostViewModel,
accountViewModel: AccountViewModel,
draftTag: String?,
) {
val tagger = val tagger =
NewMessageTagger( NewMessageTagger(
message = newPostModel.message.text, message = newPostModel.message.text,
@ -343,7 +360,7 @@ fun ChannelScreen(
mentions = tagger.pTags, mentions = tagger.pTags,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null, draftTag = draftTag,
) )
} else if (channel is LiveActivitiesChannel) { } else if (channel is LiveActivitiesChannel) {
accountViewModel.account.sendLiveMessage( accountViewModel.account.sendLiveMessage(
@ -353,24 +370,15 @@ fun ChannelScreen(
mentions = tagger.pTags, mentions = tagger.pTags,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null, draftTag = draftTag,
) )
} }
newPostModel.message = TextFieldValue("")
replyTo.value = null
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
feedViewModel.sendToTop()
}
}
}
} }
@Composable @Composable
fun DisplayReplyingToNote( fun DisplayReplyingToNote(
replyingNote: Note?, replyingNote: Note?,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
newPostModel: NewPostViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
@ -389,7 +397,6 @@ fun DisplayReplyingToNote(
null, null,
innerQuote = true, innerQuote = true,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
onWantsToReply = {}, onWantsToReply = {},
) )

View File

@ -57,6 +57,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -116,8 +117,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.findURLs
@ -238,20 +237,8 @@ fun PrepareChatroomViewModels(
if (newPostModel.requiresNIP24) { if (newPostModel.requiresNIP24) {
newPostModel.nip24 = true newPostModel.nip24 = true
} }
room.users.forEach {
newPostModel.toUsers = TextFieldValue(newPostModel.toUsers.text + " @${Hex.decode(it).toNpub()}")
}
LaunchedEffect(key1 = newPostModel) { LaunchedEffect(key1 = newPostModel) {
launch(Dispatchers.IO) {
newPostModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
newPostModel.sendPost(localDraft = newPostModel.draftTag)
}
}
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val hasNIP24 = val hasNIP24 =
accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any { accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any {
@ -333,28 +320,56 @@ fun ChatroomScreen(
RefreshingChatroomFeedView( RefreshingChatroomFeedView(
viewModel = feedViewModel, viewModel = feedViewModel,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav, nav = nav,
routeForLastRead = "Room/${room.hashCode()}", routeForLastRead = "Room/${room.hashCode()}",
avoidDraft = newPostModel.draftTag,
onWantsToReply = { onWantsToReply = {
replyTo.value = it replyTo.value = it
newPostModel.originalNote = it
}, },
) )
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } } replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(key1 = newPostModel.draftTag) {
launch(Dispatchers.IO) {
newPostModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
innerSendPost(newPostModel, room, replyTo, accountViewModel, newPostModel.draftTag)
}
}
}
// LAST ROW // LAST ROW
PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) { PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
innerSendPost(newPostModel, room, replyTo, accountViewModel, null)
accountViewModel.deleteDraft(newPostModel.draftTag) accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.message = TextFieldValue("")
newPostModel.draftTag = UUID.randomUUID().toString() newPostModel.draftTag = UUID.randomUUID().toString()
replyTo.value = null
feedViewModel.sendToTop()
}
}
}
}
private fun innerSendPost(
newPostModel: NewPostViewModel,
room: ChatroomKey,
replyTo: MutableState<Note?>,
accountViewModel: AccountViewModel,
dTag: String?,
) {
val urls = findURLs(newPostModel.message.text) val urls = findURLs(newPostModel.message.text)
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
@ -366,7 +381,7 @@ fun ChatroomScreen(
mentions = null, mentions = null,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null, draftTag = dTag,
) )
} else { } else {
accountViewModel.account.sendPrivateMessage( accountViewModel.account.sendPrivateMessage(
@ -376,16 +391,9 @@ fun ChatroomScreen(
mentions = null, mentions = null,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments, nip94attachments = usedAttachments,
draftTag = null, draftTag = dTag,
) )
} }
newPostModel.message = TextFieldValue("")
replyTo.value = null
feedViewModel.sendToTop()
}
}
}
} }
@Composable @Composable

View File

@ -0,0 +1,82 @@
/**
* 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.ui.screen.loggedIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDraftEventsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
@Composable
fun DraftListScreen(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val draftFeedViewModel: NostrDraftEventsFeedViewModel =
viewModel(
key = "NostrDraftEventsFeedViewModel",
factory = NostrDraftEventsFeedViewModel.Factory(accountViewModel.account),
)
RenderDraftListScreen(draftFeedViewModel, accountViewModel, nav)
}
@Composable
private fun RenderDraftListScreen(
feedViewModel: NostrDraftEventsFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(feedViewModel) {
feedViewModel.invalidateData()
}
DisposableEffect(lifeCycleOwner) {
val observer =
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
println("DraftList Start")
feedViewModel.invalidateData()
}
if (event == Lifecycle.Event.ON_PAUSE) {
println("DraftList Stop")
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
}
RefresheableFeedView(
feedViewModel,
null,
accountViewModel = accountViewModel,
nav = nav,
)
}

View File

@ -293,6 +293,7 @@
<string name="block_only">Block</string> <string name="block_only">Block</string>
<string name="bookmarks">Bookmarks</string> <string name="bookmarks">Bookmarks</string>
<string name="drafts">Drafts</string>
<string name="private_bookmarks">Private Bookmarks</string> <string name="private_bookmarks">Private Bookmarks</string>
<string name="public_bookmarks">Public Bookmarks</string> <string name="public_bookmarks">Public Bookmarks</string>
<string name="add_to_private_bookmarks">Add to Private Bookmarks</string> <string name="add_to_private_bookmarks">Add to Private Bookmarks</string>

View File

@ -68,6 +68,7 @@ class GiftWrapReceivingBenchmark {
markAsSensitive = true, markAsSensitive = true,
zapRaiserAmount = 10000, zapRaiserAmount = 10000,
geohash = null, geohash = null,
isDraft = true,
signer = sender, signer = sender,
) { ) {
SealedGossipEvent.create( SealedGossipEvent.create(
@ -107,6 +108,7 @@ class GiftWrapReceivingBenchmark {
markAsSensitive = true, markAsSensitive = true,
zapRaiserAmount = 10000, zapRaiserAmount = 10000,
geohash = null, geohash = null,
isDraft = true,
signer = sender, signer = sender,
) { ) {
SealedGossipEvent.create( SealedGossipEvent.create(

View File

@ -21,8 +21,8 @@
package com.vitorpamplona.quartz.events package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@ -35,126 +35,171 @@ class DraftEvent(
content: String, content: String,
sig: HexKey, sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var decryptedContent: Map<HexKey, Event> = mapOf() @Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
@Transient private var citedNotesCache: Set<String>? = null override fun isContentEncoded() = true
fun replyTos(): List<HexKey> { fun isDeleted() = content == ""
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) fun preCachedDraft(signer: NostrSigner): Event? {
return cachedInnerEvent[signer.pubKey]
return if (newStyleReplyTos.isNotEmpty()) {
newStyleReplyTos
} else {
oldStylePositional
}
} }
fun findCitations(): Set<HexKey> { fun allCache() = cachedInnerEvent.values
citedNotesCache?.let {
return it fun addToCache(
pubKey: HexKey,
innerEvent: Event,
) {
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, innerEvent)
} }
val citations = mutableSetOf<HexKey>() fun cachedDraft(
// 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, signer: NostrSigner,
onReady: (Event) -> Unit, onReady: (Event) -> Unit,
) { ) {
decryptedContent[dTag()]?.let { cachedInnerEvent[signer.pubKey]?.let {
onReady(it) onReady(it)
return return
} }
decrypt(signer) { draft ->
addToCache(signer.pubKey, draft)
signer.nip44Decrypt(content, signer.pubKey) { retVal -> onReady(draft)
val event = runCatching { fromJson(retVal) }.getOrNull() ?: return@nip44Decrypt }
decryptedContent = decryptedContent + Pair(dTag(), event) }
onReady(event) private fun decrypt(
signer: NostrSigner,
onReady: (Event) -> Unit,
) {
try {
plainContent(signer) { onReady(fromJson(it)) }
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
}
}
private fun plainContent(
signer: NostrSigner,
onReady: (String) -> Unit,
) {
if (content.isEmpty()) return
signer.nip44Decrypt(content, pubKey, onReady)
}
fun createDeletedEvent(
signer: NostrSigner,
onReady: (DraftEvent) -> Unit,
) {
signer.sign<DraftEvent>(createdAt, KIND, tags, "") {
onReady(it)
} }
} }
companion object { companion object {
const val KIND = 31234 const val KIND = 31234
fun createAddressTag(
pubKey: HexKey,
dTag: String,
): String {
return ATag(KIND, pubKey, dTag, null).toTag()
}
fun create( fun create(
dTag: String, dTag: String,
originalNote: EventInterface, originalNote: LiveActivitiesChatMessageEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag())) }
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
create(dTag, originalNote, emptyList(), signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: ChannelMessageEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
originalNote.channel()?.let { tags.add(arrayOf("e", it)) }
create(dTag, originalNote, tags, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: GitReplyEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) }
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
create(dTag, originalNote, tags, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: PollNoteEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tagsWithMarkers =
originalNote.tags().filter {
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
}
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: TextNoteEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tagsWithMarkers =
originalNote.tags().filter {
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
}
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
}
fun create(
dTag: String,
innerEvent: Event,
anchorTagArray: List<Array<String>> = emptyList(),
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit, onReady: (DraftEvent) -> Unit,
) { ) {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", dTag)) tags.add(arrayOf("d", dTag))
tags.add(arrayOf("k", "${originalNote.kind()}")) tags.add(arrayOf("k", "${innerEvent.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 -> if (anchorTagArray.isNotEmpty()) {
signer.sign(createdAt, KIND, tags.toTypedArray(), encryptedContent, onReady) tags.addAll(anchorTagArray)
}
signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey) { encryptedContent ->
signer.sign<DraftEvent>(createdAt, KIND, tags.toTypedArray(), encryptedContent) {
it.addToCache(signer.pubKey, innerEvent)
onReady(it)
}
} }
} }
} }

View File

@ -97,6 +97,8 @@ open class Event(
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] } override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
override fun firstTaggedK() = tags.firstOrNull { it.size > 1 && it[0] == "k" }?.let { it[1].toIntOrNull() }
override fun firstTaggedAddress() = override fun firstTaggedAddress() =
tags tags
.firstOrNull { it.size > 1 && it[0] == "a" } .firstOrNull { it.size > 1 && it[0] == "a" }

View File

@ -133,6 +133,8 @@ interface EventInterface {
fun firstTaggedUrl(): String? fun firstTaggedUrl(): String?
fun firstTaggedK(): Int?
fun taggedEmojis(): List<EmojiUrl> fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean fun matchTag1With(text: String): Boolean