mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-06-25 23:41:07 +02:00
- 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:
parent
cd84c07fcc
commit
6e1418cd54
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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}")
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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 = {
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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)) },
|
||||||
|
@ -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}",
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -52,7 +52,6 @@ fun WatchNoteEvent(
|
|||||||
LongPressToQuickAction(
|
LongPressToQuickAction(
|
||||||
baseNote = baseNote,
|
baseNote = baseNote,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
newPostViewModel = null,
|
|
||||||
) { showPopup ->
|
) { showPopup ->
|
||||||
BlankNote(
|
BlankNote(
|
||||||
remember {
|
remember {
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
@ -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 = {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" }
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user