Merge branch 'vitorpamplona:main' into profiles-list-management
This commit is contained in:
@@ -46,7 +46,6 @@ import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
@@ -120,7 +119,7 @@ class Amethyst : Application() {
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client, applicationIOScope)
|
||||
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
// val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
|
||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
|
||||
@@ -131,16 +132,13 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.reply
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
@@ -161,12 +159,10 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
@@ -219,6 +215,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import kotlin.collections.forEach
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Stable
|
||||
@@ -299,6 +296,7 @@ class Account(
|
||||
val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope)
|
||||
|
||||
// Relay settings
|
||||
val homeRelays = AccountHomeRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||
val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope)
|
||||
val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||
val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope)
|
||||
@@ -518,10 +516,8 @@ class Account(
|
||||
|
||||
suspend fun calculateIfNoteWasZappedByAccount(
|
||||
zappedNote: Note?,
|
||||
onWasZapped: () -> Unit,
|
||||
) {
|
||||
zappedNote?.isZappedBy(userProfile(), this, onWasZapped)
|
||||
}
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean = zappedNote?.isZappedBy(userProfile(), afterTimeInSeconds, this) == true
|
||||
|
||||
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState)
|
||||
|
||||
@@ -1161,103 +1157,42 @@ class Account(
|
||||
return event
|
||||
}
|
||||
|
||||
suspend fun <T : Event> signAndSend(
|
||||
draftTag: String?,
|
||||
template: EventTemplate<T>,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
broadcastNotes: List<Entity>,
|
||||
) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet())
|
||||
|
||||
suspend fun <T : Event> signAndSend(
|
||||
draftTag: String?,
|
||||
template: EventTemplate<T>,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
broadcastNotes: Set<Note>,
|
||||
) {
|
||||
if (draftTag != null) {
|
||||
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
|
||||
val draftEvent =
|
||||
DraftEvent.create(
|
||||
dTag = draftTag,
|
||||
innerEvent = rumor,
|
||||
anchorTagArray = emptyList(),
|
||||
signer = signer,
|
||||
)
|
||||
draftsDecryptionCache.preload(draftEvent, rumor)
|
||||
sendDraftEvent(draftEvent)
|
||||
} else {
|
||||
val it = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
client.send(it, relayList = relayList)
|
||||
|
||||
broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Event> signAndSendNIP04Message(
|
||||
draftTag: String?,
|
||||
template: EventTemplate<T>,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
if (draftTag != null) {
|
||||
if (template.content.isEmpty()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
|
||||
val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer)
|
||||
draftsDecryptionCache.preload(draftEvent, rumor)
|
||||
sendDraftEvent(draftEvent)
|
||||
}
|
||||
} else {
|
||||
val newEvent = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(newEvent)
|
||||
client.send(newEvent, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Event> signAndSendWithList(
|
||||
draftTag: String?,
|
||||
template: EventTemplate<T>,
|
||||
relayList: Collection<NormalizedRelayUrl>,
|
||||
broadcastNotes: Set<Note>,
|
||||
) {
|
||||
if (draftTag != null) {
|
||||
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
|
||||
val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer)
|
||||
draftsDecryptionCache.preload(draftEvent, rumor)
|
||||
sendDraftEvent(draftEvent)
|
||||
} else {
|
||||
val event = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
|
||||
val relaySet = relayList.toSet()
|
||||
|
||||
client.send(event, relayList = relaySet)
|
||||
broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relaySet) } }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createAndSendDraft(
|
||||
draftTag: String,
|
||||
template: EventTemplate<out Event>,
|
||||
broadcast: Set<Event> = emptySet(),
|
||||
) {
|
||||
val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList()
|
||||
|
||||
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
|
||||
val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer)
|
||||
draftsDecryptionCache.preload(draftEvent, rumor)
|
||||
sendDraftEvent(draftEvent)
|
||||
|
||||
cache.justConsumeMyOwnEvent(draftEvent)
|
||||
|
||||
val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet()
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(draftEvent, relayList)
|
||||
broadcast.forEach {
|
||||
client.send(it, relayList.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteDraft(draftTag: String) {
|
||||
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
|
||||
cache.getAddressableNoteIfExists(key)?.let { note ->
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is DraftEvent) {
|
||||
val deletedDraftEvent = noteEvent.createDeletedEvent(signer)
|
||||
client.send(deletedDraftEvent, outboxRelays.flow.value + note.relays)
|
||||
cache.justConsumeMyOwnEvent(deletedDraftEvent)
|
||||
val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList()
|
||||
|
||||
delete(deletedDraftEvent, note.relays.toSet())
|
||||
}
|
||||
val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer)
|
||||
val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft)))
|
||||
|
||||
val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet()
|
||||
|
||||
cache.justConsumeMyOwnEvent(deletedDraft)
|
||||
cache.justConsumeMyOwnEvent(deletionEvent)
|
||||
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(deletedDraft, relayList)
|
||||
client.send(deletionEvent, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1277,7 +1212,18 @@ class Account(
|
||||
currentSceneRelay = readingSceneRelay,
|
||||
)
|
||||
|
||||
sendToPrivateOutboxAndLocal(signer.sign(template))
|
||||
val event = signer.sign(template)
|
||||
|
||||
// updates relays that already have this replaceable.
|
||||
val noteRelays = cache.getAddressableNoteIfExists(event.address())?.relays ?: emptyList()
|
||||
|
||||
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(event, relayList + noteRelays)
|
||||
} else {
|
||||
client.send(event, outboxRelays.flow.value + noteRelays)
|
||||
}
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun updateInteractiveStoryReadingState(
|
||||
@@ -1294,7 +1240,18 @@ class Account(
|
||||
currentSceneRelay = readingSceneRelay,
|
||||
)
|
||||
|
||||
sendToPrivateOutboxAndLocal(signer.sign(template))
|
||||
val event = signer.sign(template)
|
||||
|
||||
// updates relays that already have this replaceable.
|
||||
val noteRelays = cache.getAddressableNoteIfExists(event.address())?.relays ?: emptyList()
|
||||
|
||||
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(event, relayList + noteRelays)
|
||||
} else {
|
||||
client.send(event, outboxRelays.flow.value + noteRelays)
|
||||
}
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
fun mapEntitiesToNotes(entities: List<Entity>): List<Note> =
|
||||
@@ -1348,7 +1305,15 @@ class Account(
|
||||
contentWarningReason?.let { contentWarning(contentWarningReason) }
|
||||
}
|
||||
|
||||
signAndSend(draftTag, template, relayList, quotes)
|
||||
if (draftTag != null) {
|
||||
createAndSendDraft(draftTag, template)
|
||||
} else {
|
||||
val it = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
client.send(it, relayList = relayList)
|
||||
|
||||
mapEntitiesToNotes(quotes).forEach { it.event?.let { client.send(it, relayList = relayList) } }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendInteractiveStoryScene(
|
||||
@@ -1383,29 +1348,42 @@ class Account(
|
||||
contentWarningReason?.let { contentWarning(contentWarningReason) }
|
||||
}
|
||||
|
||||
signAndSend(draftTag, template, relayList, mapEntitiesToNotes(quotes).toSet())
|
||||
val broadcastNotes = mapEntitiesToNotes(quotes).toSet()
|
||||
|
||||
if (draftTag != null) {
|
||||
createAndSendDraft(draftTag, template)
|
||||
} else {
|
||||
val it = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(it)
|
||||
client.send(it, relayList = relayList)
|
||||
|
||||
broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendAddBounty(
|
||||
value: BigDecimal,
|
||||
bounty: Note,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val event = bounty.event as? TextNoteEvent ?: return
|
||||
val eventAuthor = bounty.author ?: return
|
||||
val bountyEvent = bounty.event as? TextNoteEvent ?: return
|
||||
val bountyAuthor = bounty.author ?: return
|
||||
|
||||
val template =
|
||||
BountyAddValueEvent.build(
|
||||
value,
|
||||
EventHintBundle(event, bounty.relayHintUrl()),
|
||||
eventAuthor.toPTag(),
|
||||
amount = value,
|
||||
bountyRoot = EventHintBundle(bountyEvent, bounty.relayHintUrl()),
|
||||
bountyRootAuthor = bountyAuthor.toPTag(),
|
||||
)
|
||||
|
||||
val relays = bounty.relays + outboxRelays.flow.value
|
||||
val relays = (bounty.relays + outboxRelays.flow.value).toSet()
|
||||
|
||||
signAndSendWithList(draftTag, template, relays, setOf(bounty))
|
||||
val newEvent = signer.sign(template)
|
||||
cache.justConsumeMyOwnEvent(newEvent)
|
||||
|
||||
client.send(newEvent, relayList = relays)
|
||||
client.send(bountyEvent, relayList = relays)
|
||||
}
|
||||
|
||||
suspend fun sendEdit(
|
||||
@@ -1437,82 +1415,27 @@ class Account(
|
||||
broadcast.forEach { client.send(it, relayList) }
|
||||
}
|
||||
|
||||
suspend fun sendPrivateMessage(
|
||||
message: String,
|
||||
toUser: PTag,
|
||||
replyingTo: Note? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
contentWarningReason: String? = null,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
emojis: List<EmojiUrlTag>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val encryptedContent =
|
||||
signer.nip04Encrypt(
|
||||
PrivateDmEvent.prepareMessageToEncrypt(message, imetas),
|
||||
toUser.pubKey,
|
||||
)
|
||||
val newEvent = signer.sign(eventTemplate)
|
||||
val recipient = newEvent.verifiedRecipientPubKey()
|
||||
val destinationRelays = recipient?.let { cache.getOrCreateUser(it).dmInboxRelays() } ?: emptyList()
|
||||
|
||||
val template =
|
||||
PrivateDmEvent.build(toUser, encryptedContent) {
|
||||
replyingTo?.let { reply(it.toEId()) }
|
||||
|
||||
geohash?.let { geohash(it) }
|
||||
zapRaiserAmount?.let { zapraiser(it) }
|
||||
zapReceiver?.let { zapSplits(it) }
|
||||
emojis?.let { emojis(it) }
|
||||
contentWarningReason?.let { contentWarning(contentWarningReason) }
|
||||
cache.justConsumeMyOwnEvent(newEvent)
|
||||
client.send(newEvent, outboxRelays.flow.value + destinationRelays)
|
||||
}
|
||||
|
||||
val destinationRelays = cache.getOrCreateUser(toUser.pubKey).dmInboxRelays()
|
||||
|
||||
signAndSendNIP04Message(draftTag, template, outboxRelays.flow.value + destinationRelays)
|
||||
}
|
||||
|
||||
suspend fun sendNIP17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) {
|
||||
suspend fun sendNip17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val wraps = NIP17Factory().createEncryptedFileNIP17(template, signer)
|
||||
broadcastPrivately(wraps)
|
||||
}
|
||||
|
||||
suspend fun sendNIP17PrivateMessage(
|
||||
template: EventTemplate<ChatMessageEvent>,
|
||||
draftTag: String? = null,
|
||||
) {
|
||||
if (draftTag != null) {
|
||||
if (template.content.isEmpty()) {
|
||||
deleteDraft(draftTag)
|
||||
} else {
|
||||
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
|
||||
val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer)
|
||||
draftsDecryptionCache.preload(draftEvent, rumor)
|
||||
sendDraftEvent(draftEvent)
|
||||
}
|
||||
} else {
|
||||
val it = NIP17Factory().createMessageNIP17(template, signer)
|
||||
broadcastPrivately(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendDraftEvent(draftEvent: DraftEvent) {
|
||||
sendToPrivateOutboxAndLocal(draftEvent)
|
||||
}
|
||||
|
||||
fun sendToPrivateOutboxAndLocal(event: Event?) {
|
||||
if (event == null) return
|
||||
|
||||
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
|
||||
if (relayList.isNotEmpty()) {
|
||||
client.send(event, relayList.toSet())
|
||||
} else {
|
||||
client.send(event, outboxRelays.flow.value)
|
||||
}
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) {
|
||||
val events = NIP17Factory().createMessageNIP17(template, signer)
|
||||
broadcastPrivately(events)
|
||||
}
|
||||
|
||||
suspend fun broadcastPrivately(signedEvents: NIP17Factory.Result) {
|
||||
@@ -1665,7 +1588,7 @@ class Account(
|
||||
privateDMDecryptionCache.cachedDM(event)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap()) {
|
||||
privateZapsDecryptionCache.cachedPrivateZap(event)?.content
|
||||
} else if (event is DraftEvent) {
|
||||
} else if (event is DraftWrapEvent) {
|
||||
draftsDecryptionCache.preCachedDraft(event)?.content
|
||||
} else {
|
||||
event.content
|
||||
@@ -1689,7 +1612,7 @@ class Account(
|
||||
} else {
|
||||
event.content
|
||||
}
|
||||
} else if (event is DraftEvent && isWriteable()) {
|
||||
} else if (event is DraftWrapEvent && isWriteable()) {
|
||||
draftsDecryptionCache.cachedDraft(event)?.content
|
||||
} else {
|
||||
event?.content
|
||||
|
||||
@@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
@@ -89,7 +89,7 @@ val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Co
|
||||
val DefaultSignerPermissions =
|
||||
listOf(
|
||||
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND),
|
||||
Permission(CommandType.SIGN_EVENT, DraftEvent.KIND),
|
||||
Permission(CommandType.SIGN_EVENT, DraftWrapEvent.KIND),
|
||||
Permission(CommandType.NIP04_ENCRYPT),
|
||||
Permission(CommandType.NIP04_DECRYPT),
|
||||
Permission(CommandType.NIP44_DECRYPT),
|
||||
|
||||
@@ -22,10 +22,14 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.ListChange
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.LargeCache
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Stable
|
||||
abstract class Channel : NotesGatherer {
|
||||
@@ -34,6 +38,16 @@ abstract class Channel : NotesGatherer {
|
||||
|
||||
private var relays = mapOf<NormalizedRelayUrl, Counter>()
|
||||
|
||||
private var changesFlow: WeakReference<MutableSharedFlow<ListChange<Note>>> = WeakReference(null)
|
||||
|
||||
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
|
||||
val current = changesFlow.get()
|
||||
if (current != null) return current
|
||||
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
|
||||
changesFlow = WeakReference(new)
|
||||
return new
|
||||
}
|
||||
|
||||
abstract fun toBestDisplayName(): String
|
||||
|
||||
open fun relays(): Set<NormalizedRelayUrl> =
|
||||
@@ -80,6 +94,8 @@ abstract class Channel : NotesGatherer {
|
||||
addRelay(relay)
|
||||
}
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.Addition(note))
|
||||
|
||||
flowSet?.notes?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -93,6 +109,8 @@ abstract class Channel : NotesGatherer {
|
||||
lastNote = notes.values().sortedWith(DefaultFeedOrder).firstOrNull()
|
||||
}
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.Deletion(note))
|
||||
|
||||
flowSet?.notes?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -109,6 +127,8 @@ abstract class Channel : NotesGatherer {
|
||||
|
||||
toBeRemoved.forEach { notes.remove(it.idHex) }
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.SetDeletion(toBeRemoved.toSet()))
|
||||
|
||||
flowSet?.notes?.invalidateData()
|
||||
|
||||
return toBeRemoved.toSet()
|
||||
@@ -123,6 +143,8 @@ abstract class Channel : NotesGatherer {
|
||||
|
||||
hidden.forEach { notes.remove(it.idHex) }
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.SetDeletion(hidden))
|
||||
|
||||
flowSet?.notes?.invalidateData()
|
||||
|
||||
return hidden.toSet()
|
||||
|
||||
@@ -120,7 +120,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
@@ -2549,7 +2549,7 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: DraftEvent,
|
||||
event: DraftWrapEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified)
|
||||
@@ -2757,7 +2757,7 @@ object LocalCache : ILocalCache {
|
||||
is CommunityPostApprovalEvent -> consume(event, relay, wasVerified)
|
||||
is ContactListEvent -> consume(event, relay, wasVerified)
|
||||
is DeletionEvent -> consume(event, relay, wasVerified)
|
||||
is DraftEvent -> consume(event, relay, wasVerified)
|
||||
is DraftWrapEvent -> consume(event, relay, wasVerified)
|
||||
is EmojiPackEvent -> consume(event, relay, wasVerified)
|
||||
is EmojiPackSelectionEvent -> consume(event, relay, wasVerified)
|
||||
is EphemeralChatEvent -> consume(event, relay, wasVerified)
|
||||
|
||||
@@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
@@ -235,7 +235,7 @@ open class Note(
|
||||
|
||||
open fun createdAt() = event?.createdAt
|
||||
|
||||
fun isDraft() = event is DraftEvent
|
||||
fun isDraft() = event is DraftWrapEvent
|
||||
|
||||
fun loadEvent(
|
||||
event: Event,
|
||||
@@ -472,28 +472,28 @@ open class Note(
|
||||
}
|
||||
|
||||
private suspend fun isPaidByCalculation(
|
||||
zapPayments: List<Pair<Note, Note?>>,
|
||||
afterTimeInSeconds: Long,
|
||||
account: Account,
|
||||
zapEvents: List<Pair<Note, Note?>>,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
if (zapEvents.isEmpty()) {
|
||||
return
|
||||
): Boolean {
|
||||
if (zapPayments.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
var hasSentOne = false
|
||||
|
||||
launchAndWaitAll(zapEvents) { next ->
|
||||
return anyAsync(zapPayments) { next ->
|
||||
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
|
||||
|
||||
if (zapResponseEvent != null) {
|
||||
account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response ->
|
||||
val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor())
|
||||
|
||||
if (!hasSentOne && result == true) {
|
||||
hasSentOne = true
|
||||
onWasZappedByAuthor()
|
||||
}
|
||||
val response = account.nip47SignerState.decryptResponse(zapResponseEvent)
|
||||
if (response != null) {
|
||||
response is PayInvoiceSuccessResponse &&
|
||||
account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) &&
|
||||
zapResponseEvent.createdAt > afterTimeInSeconds
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,25 +501,28 @@ open class Note(
|
||||
private suspend fun isZappedByCalculation(
|
||||
option: Int?,
|
||||
user: User,
|
||||
afterTimeInSeconds: Long,
|
||||
account: Account,
|
||||
zapEvents: Map<Note, Note?>,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
): Boolean {
|
||||
if (zapEvents.isEmpty()) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent?>>()
|
||||
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent>>()
|
||||
|
||||
zapEvents.forEach { next ->
|
||||
val zapRequest = next.key.event as LnZapRequestEvent
|
||||
val zapEvent = next.value?.event as? LnZapEvent
|
||||
|
||||
if (zapEvent != null) {
|
||||
if (!zapRequest.isPrivateZap()) {
|
||||
// public events
|
||||
if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||
onWasZappedByAuthor()
|
||||
return
|
||||
if (zapRequest.pubKey == user.pubkeyHex &&
|
||||
zapEvent.createdAt > afterTimeInSeconds &&
|
||||
(option == null || option == zapEvent.zappedPollOption())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// private events
|
||||
@@ -527,9 +530,11 @@ open class Note(
|
||||
// if has already decrypted
|
||||
val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest)
|
||||
if (privateZap != null) {
|
||||
if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||
onWasZappedByAuthor()
|
||||
return
|
||||
if (privateZap.pubKey == user.pubkeyHex &&
|
||||
zapEvent.createdAt > afterTimeInSeconds &&
|
||||
(option == null || option == zapEvent.zappedPollOption())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (account.isWriteable()) {
|
||||
@@ -538,38 +543,39 @@ open class Note(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result =
|
||||
anyAsync(parallelDecrypt) { pair ->
|
||||
val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first)
|
||||
|
||||
result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption())
|
||||
}
|
||||
|
||||
if (result) {
|
||||
onWasZappedByAuthor()
|
||||
if (parallelDecrypt.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return anyAsync(parallelDecrypt) { pair ->
|
||||
val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first)
|
||||
result?.pubKey == user.pubkeyHex &&
|
||||
pair.second.createdAt > afterTimeInSeconds &&
|
||||
(option == null || option == pair.second.zappedPollOption())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isZappedBy(
|
||||
user: User,
|
||||
afterTimeInSeconds: Long,
|
||||
account: Account,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor)
|
||||
): Boolean {
|
||||
val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps)
|
||||
if (first) return true
|
||||
if (account.userProfile() == user) {
|
||||
isPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
||||
return isPaidByCalculation(zapPayments.toList(), afterTimeInSeconds, account)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun isZappedBy(
|
||||
option: Int?,
|
||||
user: User,
|
||||
afterTimeInSeconds: Long,
|
||||
account: Account,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
|
||||
}
|
||||
): Boolean = isZappedByCalculation(option, user, afterTimeInSeconds, account, zaps)
|
||||
|
||||
fun getReactionBy(user: User): String? =
|
||||
reactions.firstNotNullOfOrNull {
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class AccountHomeRelayState(
|
||||
nip65: Nip65RelayListState,
|
||||
privateStorage: PrivateStorageRelayListState,
|
||||
local: LocalRelayListState,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
val flow =
|
||||
combine(
|
||||
nip65.outboxFlow,
|
||||
privateStorage.flow,
|
||||
local.flow,
|
||||
) { nip65Outbox, privateOutBox, localRelays ->
|
||||
nip65Outbox + privateOutBox + localRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
nip65.outboxFlow.value +
|
||||
privateStorage.flow.value +
|
||||
local.flow.value,
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -44,8 +44,8 @@ class AccountOutboxRelayState(
|
||||
privateStorage.flow,
|
||||
local.flow,
|
||||
broadcast.flow,
|
||||
) { nip65Inbox, privateOutBox, localRelays, broadcastRelays ->
|
||||
nip65Inbox + privateOutBox + localRelays + broadcastRelays
|
||||
) { nip65Outbox, privateOutBox, localRelays, broadcastRelays ->
|
||||
nip65Outbox + privateOutBox + localRelays + broadcastRelays
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
|
||||
+2
-2
@@ -131,9 +131,9 @@ class FollowListOutboxOrProxyRelays(
|
||||
val flowSet: StateFlow<Set<String>> =
|
||||
flow
|
||||
.map { relayList ->
|
||||
relayList.map { it.url }.toSet()
|
||||
relayList.mapTo(mutableSetOf()) { it.url }
|
||||
}.onStart {
|
||||
emit(flow.value.map { it.url }.toSet())
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.url })
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope,
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ class NwcSignerState(
|
||||
val walletService = settings.zapPaymentRequest.value
|
||||
if (walletService == null) throw IllegalArgumentException("No NIP47 setup")
|
||||
|
||||
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, signer)
|
||||
val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value)
|
||||
|
||||
val filter =
|
||||
NWCPaymentQueryState(
|
||||
|
||||
@@ -25,11 +25,15 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NotesGatherer
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.ListChange
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Stable
|
||||
class Chatroom : NotesGatherer {
|
||||
@@ -38,7 +42,17 @@ class Chatroom : NotesGatherer {
|
||||
var subject = MutableStateFlow<String?>(null)
|
||||
var subjectCreatedAt: Long? = null
|
||||
var ownerSentMessage: Boolean = false
|
||||
var lastMessage: Note? = null
|
||||
var newestMessage: Note? = null
|
||||
|
||||
private var changesFlow: WeakReference<MutableSharedFlow<ListChange<Note>>> = WeakReference(null)
|
||||
|
||||
fun changesFlow(): MutableSharedFlow<ListChange<Note>> {
|
||||
val current = changesFlow.get()
|
||||
if (current != null) return current
|
||||
val new = MutableSharedFlow<ListChange<Note>>(0, 100, BufferOverflow.DROP_OLDEST)
|
||||
changesFlow = WeakReference(new)
|
||||
return new
|
||||
}
|
||||
|
||||
override fun removeNote(note: Note) {
|
||||
removeMessageSync(note)
|
||||
@@ -57,8 +71,8 @@ class Chatroom : NotesGatherer {
|
||||
}
|
||||
|
||||
val createdAt = msg.createdAt() ?: 0
|
||||
if (createdAt > (lastMessage?.createdAt() ?: 0)) {
|
||||
lastMessage = msg
|
||||
if (createdAt > (newestMessage?.createdAt() ?: 0)) {
|
||||
newestMessage = msg
|
||||
}
|
||||
|
||||
val newSubject = msg.event?.subject()
|
||||
@@ -67,6 +81,9 @@ class Chatroom : NotesGatherer {
|
||||
subject.tryEmit(newSubject)
|
||||
subjectCreatedAt = msg.createdAt()
|
||||
}
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.Addition(msg))
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -78,14 +95,27 @@ class Chatroom : NotesGatherer {
|
||||
messages = messages - msg
|
||||
msg.removeGatherer(this)
|
||||
|
||||
if (msg == newestMessage) {
|
||||
newestMessage = messages.maxByOrNull { it.createdAt() ?: 0 }
|
||||
}
|
||||
|
||||
if (msg.event?.subject() == subject.value) {
|
||||
messages
|
||||
.filter { it.event?.subject() != null }
|
||||
.sortedBy { it.createdAt() }
|
||||
.lastOrNull()
|
||||
?.let {
|
||||
.maxByOrNull {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent?.subject() != null) {
|
||||
noteEvent.createdAt
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}?.let {
|
||||
subject.tryEmit(it.event?.subject())
|
||||
subjectCreatedAt = it.createdAt()
|
||||
}
|
||||
}
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.Deletion(msg))
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -107,6 +137,9 @@ class Chatroom : NotesGatherer {
|
||||
|
||||
val toRemove = messages.minus(toKeep)
|
||||
messages = toKeep
|
||||
|
||||
changesFlow.get()?.tryEmit(ListChange.SetDeletion<Note>(toRemove))
|
||||
|
||||
return toRemove
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ class DualHttpClientManager(
|
||||
factory.buildHttpClient(proxy, mobile, userAgent)
|
||||
}.stateIn(
|
||||
scope,
|
||||
SharingStarted.Lazily,
|
||||
SharingStarted.WhileSubscribed(1000),
|
||||
factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent),
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ class DualHttpClientManager(
|
||||
factory.buildHttpClient(mobile, userAgent)
|
||||
}.stateIn(
|
||||
scope,
|
||||
SharingStarted.Lazily,
|
||||
SharingStarted.WhileSubscribed(1000),
|
||||
factory.buildHttpClient(isMobileDataProvider.value, userAgent),
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class AccountMetadataEoseManager(
|
||||
) : PerUserEoseManager<AccountQueryState>(client, allKeys) {
|
||||
override fun user(key: AccountQueryState) = key.account.userProfile()
|
||||
|
||||
fun relayFlow(query: AccountQueryState) = query.account.outboxRelays.flow
|
||||
fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow
|
||||
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
|
||||
+2
-2
@@ -24,13 +24,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
|
||||
val ReportsAndBookmarksFromKeyKinds =
|
||||
listOf(
|
||||
DraftEvent.KIND,
|
||||
DraftWrapEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -809,9 +808,6 @@ private fun verifyHash(content: MediaUrlContent): Boolean? {
|
||||
|
||||
Amethyst.instance.diskCache.openSnapshot(content.url)?.use { snapshot ->
|
||||
val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey()
|
||||
|
||||
Log.d("Image Hash Verification", "$hash == ${content.hash}")
|
||||
|
||||
return hash == content.hash
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,9 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
|
||||
abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
abstract fun applyFilter(newItems: Set<T>): Set<T>
|
||||
|
||||
abstract fun sort(items: Set<T>): List<T>
|
||||
|
||||
abstract class AdditiveFeedFilter<T> :
|
||||
FeedFilter<T>(),
|
||||
IAdditiveFeedFilter<T> {
|
||||
open fun updateListWith(
|
||||
oldList: List<T>,
|
||||
newItems: Set<T>,
|
||||
@@ -42,4 +40,15 @@ abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
|
||||
oldList
|
||||
}
|
||||
}
|
||||
|
||||
fun removeNotes(
|
||||
oldList: List<T>,
|
||||
deletedNotes: Set<T>,
|
||||
): List<T> = oldList - deletedNotes
|
||||
}
|
||||
|
||||
interface IAdditiveFeedFilter<T> : IFeedFilter<T> {
|
||||
fun applyFilter(newItems: Set<T>): Set<T>
|
||||
|
||||
fun sort(items: Set<T>): List<T>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.dal
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
interface ChangesFlowFilter<T> : IAdditiveFeedFilter<T> {
|
||||
fun changesFlow(): MutableSharedFlow<ListChange<T>>
|
||||
}
|
||||
|
||||
sealed class ListChange<out T> {
|
||||
data class Addition<T>(
|
||||
val item: T,
|
||||
) : ListChange<T>()
|
||||
|
||||
data class Deletion<T>(
|
||||
val item: T,
|
||||
) : ListChange<T>()
|
||||
|
||||
data class SetAddition<T>(
|
||||
val item: Set<T>,
|
||||
) : ListChange<T>()
|
||||
|
||||
data class SetDeletion<T>(
|
||||
val item: Set<T>,
|
||||
) : ListChange<T>()
|
||||
}
|
||||
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
|
||||
abstract class FeedFilter<T> {
|
||||
fun loadTop(): List<T> {
|
||||
abstract class FeedFilter<T> : IFeedFilter<T> {
|
||||
override fun loadTop(): List<T> {
|
||||
val feed =
|
||||
logTime(
|
||||
debugMessage = { "${this.javaClass.simpleName} FeedFilter returning ${it.size} objects" },
|
||||
@@ -31,13 +31,17 @@ abstract class FeedFilter<T> {
|
||||
)
|
||||
return feed.take(limit())
|
||||
}
|
||||
}
|
||||
|
||||
open fun limit() = 500
|
||||
interface IFeedFilter<T> {
|
||||
fun loadTop(): List<T>
|
||||
|
||||
fun limit(): Int = 500
|
||||
|
||||
/** Returns a string that serves as the key to invalidate the list if it changes. */
|
||||
abstract fun feedKey(): Any
|
||||
fun feedKey(): Any
|
||||
|
||||
open fun showHiddenKey(): Boolean = false
|
||||
fun showHiddenKey(): Boolean = false
|
||||
|
||||
abstract fun feed(): List<T>
|
||||
fun feed(): List<T>
|
||||
}
|
||||
|
||||
@@ -20,17 +20,16 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.IFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.ammolite.relays.BasicBundledInsert
|
||||
import com.vitorpamplona.ammolite.relays.BasicBundledUpdate
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -42,7 +41,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class FeedContentState(
|
||||
val localFilter: FeedFilter<Note>,
|
||||
val localFilter: IFeedFilter<Note>,
|
||||
val viewModelScope: CoroutineScope,
|
||||
) : InvalidatableContent {
|
||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||
@@ -177,8 +176,8 @@ class FeedContentState(
|
||||
}
|
||||
}
|
||||
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
private val bundler = BasicBundledUpdate(250, Dispatchers.IO, viewModelScope)
|
||||
private val bundlerInsert = BasicBundledInsert<Set<Note>>(250, Dispatchers.IO, viewModelScope)
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@@ -202,7 +201,9 @@ class FeedContentState(
|
||||
}
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) { refreshFromOldState(it.flatten().toSet()) }
|
||||
bundlerInsert.invalidateList(newItems) {
|
||||
refreshFromOldState(it.flatten().toSet())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFeedWith(newNotes: Set<Note>) {
|
||||
@@ -216,10 +217,4 @@ class FeedContentState(
|
||||
invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
bundlerInsert.cancel()
|
||||
bundler.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
@@ -63,7 +63,7 @@ fun routeFor(
|
||||
noteEvent: Event,
|
||||
loggedIn: Account,
|
||||
): Route? {
|
||||
if (noteEvent is DraftEvent) {
|
||||
if (noteEvent is DraftWrapEvent) {
|
||||
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
||||
|
||||
if (innerEvent is IsInPublicChatChannel) {
|
||||
|
||||
@@ -187,7 +187,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
@@ -552,7 +552,7 @@ fun InnerNoteWithReactions(
|
||||
}
|
||||
}
|
||||
|
||||
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftEvent
|
||||
val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftWrapEvent
|
||||
|
||||
if (isNotRepost) {
|
||||
if (makeItShort) {
|
||||
@@ -568,7 +568,7 @@ fun InnerNoteWithReactions(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (baseNote.event is DraftEvent) {
|
||||
if (baseNote.event is DraftWrapEvent) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
@@ -647,7 +647,7 @@ private fun RenderNoteRow(
|
||||
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
is AudioTrackEvent -> RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
is AudioHeaderEvent -> RenderAudioHeader(baseNote, ContentScale.FillWidth, accountViewModel, nav)
|
||||
is DraftEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav)
|
||||
is DraftWrapEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav)
|
||||
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
@@ -901,7 +901,7 @@ fun ObserveDraftEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
render: @Composable (Note) -> Unit,
|
||||
) {
|
||||
val noteEvent by observeNoteEvent<DraftEvent>(note, accountViewModel)
|
||||
val noteEvent by observeNoteEvent<DraftWrapEvent>(note, accountViewModel)
|
||||
|
||||
noteEvent?.let {
|
||||
val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it)
|
||||
|
||||
@@ -52,6 +52,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -90,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size14Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -100,6 +102,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -517,6 +520,8 @@ fun ZapVote(
|
||||
}
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var zapStartingTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) }
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -558,6 +563,7 @@ fun ZapVote(
|
||||
accountViewModel.zapAmountChoices().size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
|
||||
) {
|
||||
zapStartingTime = TimeUtils.now()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
accountViewModel.zapAmountChoices().first() * 1000,
|
||||
@@ -583,6 +589,7 @@ fun ZapVote(
|
||||
accountViewModel,
|
||||
pollViewModel,
|
||||
poolOption.option,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onDismiss = {
|
||||
wantsToZap = false
|
||||
zappingProgress = 0f
|
||||
@@ -660,9 +667,10 @@ fun ZapVote(
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.width(3.dp))
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { zappingProgress },
|
||||
modifier = Modifier.size(14.dp),
|
||||
modifier = Size14Modifier,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
@@ -688,6 +696,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
pollViewModel: PollNoteViewModel,
|
||||
pollOption: Int,
|
||||
onZapStarts: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String, toUser: User?) -> Unit,
|
||||
@@ -717,6 +726,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
@@ -743,6 +753,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
|
||||
@@ -103,10 +103,8 @@ class PollNoteViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
totalZapped = totalZapped()
|
||||
wasZappedByLoggedInAccount = false
|
||||
account?.calculateIfNoteWasZappedByAccount(pollNote) {
|
||||
wasZappedByLoggedInAccount = true
|
||||
wasZappedByLoggedInAccount = account.calculateIfNoteWasZappedByAccount(pollNote, 0)
|
||||
canZap.value = checkIfCanZap()
|
||||
}
|
||||
|
||||
tallies.forEach {
|
||||
val zappedValue = zappedPollOptionAmount(it.option)
|
||||
@@ -210,10 +208,8 @@ class PollNoteViewModel : ViewModel() {
|
||||
suspend fun isPollOptionZappedBy(
|
||||
option: Int,
|
||||
user: User,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor)
|
||||
}
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean = pollNote?.isZappedBy(option, user, afterTimeInSeconds, account) == true
|
||||
|
||||
fun cachedIsPollOptionZappedBy(
|
||||
option: Int,
|
||||
|
||||
@@ -66,6 +66,7 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -135,7 +136,7 @@ import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightWithPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiser
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiserWithPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size14dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size14Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
|
||||
@@ -154,11 +155,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -977,7 +980,7 @@ fun ZapReaction(
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = Size20dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
animationSize: Dp = Size14dp,
|
||||
animationModifier: Modifier = Size14Modifier,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToZap by remember { mutableStateOf(false) }
|
||||
@@ -994,6 +997,7 @@ fun ZapReaction(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var zapStartingTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
@@ -1008,6 +1012,7 @@ fun ZapReaction(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = {
|
||||
scope.launch {
|
||||
@@ -1033,6 +1038,7 @@ fun ZapReaction(
|
||||
baseNote = baseNote,
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onDismiss = {
|
||||
wantsToZap = false
|
||||
zappingProgress = 0f
|
||||
@@ -1083,6 +1089,7 @@ fun ZapReaction(
|
||||
|
||||
if (wantsToSetCustomZap) {
|
||||
ZapCustomDialog(
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onClose = { wantsToSetCustomZap = false },
|
||||
onError = { _, message, user ->
|
||||
scope.launch {
|
||||
@@ -1106,11 +1113,23 @@ fun ZapReaction(
|
||||
label = "ZapIconIndicator",
|
||||
)
|
||||
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
zapStartingTime,
|
||||
) { wasZappedByLoggedInUser ->
|
||||
CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
ZappedIcon(iconSizeModifier)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = remember { Modifier.size(animationSize) },
|
||||
modifier = animationModifier,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
@@ -1136,6 +1155,7 @@ fun zapClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
context: Context,
|
||||
onZapStarts: () -> Unit,
|
||||
onZappingProgress: (Float) -> Unit,
|
||||
onMultipleChoices: () -> Unit,
|
||||
onError: (String, String, User?) -> Unit,
|
||||
@@ -1162,6 +1182,7 @@ fun zapClick(
|
||||
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
|
||||
)
|
||||
} else if (choices.size == 1) {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
choices.first() * 1000,
|
||||
@@ -1181,6 +1202,7 @@ fun zapClick(
|
||||
fun ObserveZapIcon(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
afterTimeInSeconds: Long = 0,
|
||||
inner: @Composable (MutableState<Boolean>) -> Unit,
|
||||
) {
|
||||
val wasZappedByLoggedInUser = remember { mutableStateOf(false) }
|
||||
@@ -1196,14 +1218,13 @@ fun ObserveZapIcon(
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) {
|
||||
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped ->
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, afterTimeInSeconds)
|
||||
if (wasZappedByLoggedInUser.value != newWasZapped) {
|
||||
wasZappedByLoggedInUser.value = newWasZapped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner(wasZappedByLoggedInUser)
|
||||
}
|
||||
@@ -1552,6 +1573,7 @@ fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
onZapStarts: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String, user: User?) -> Unit,
|
||||
@@ -1562,7 +1584,7 @@ fun ZapAmountChoicePopup(
|
||||
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onZapStarts, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1571,6 +1593,7 @@ fun ZapAmountChoicePopup(
|
||||
zapAmountChoices: ImmutableList<Long>,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
onZapStarts: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String, user: User?) -> Unit,
|
||||
@@ -1578,7 +1601,7 @@ fun ZapAmountChoicePopup(
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onZapStarts, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@@ -1589,6 +1612,7 @@ fun ZapAmountChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onZapStarts: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String, user: User?) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
@@ -1615,6 +1639,7 @@ fun ZapAmountChoicePopup(
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
@@ -1641,6 +1666,7 @@ fun ZapAmountChoicePopup(
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
|
||||
@@ -103,6 +103,7 @@ class ZapOptionViewModel : ViewModel() {
|
||||
|
||||
@Composable
|
||||
fun ZapCustomDialog(
|
||||
onZapStarts: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onError: (title: String, text: String, user: User?) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
@@ -172,6 +173,7 @@ fun ZapCustomDialog(
|
||||
ZapButton(
|
||||
isActive = postViewModel.canSend() && !baseNote.isDraft(),
|
||||
) {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
postViewModel.value()!! * 1000L,
|
||||
|
||||
@@ -179,7 +179,6 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
myAccount.sendAddBounty(
|
||||
newValue,
|
||||
bountyInner,
|
||||
draftTag = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-6
@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -83,12 +84,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size14Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -285,7 +288,7 @@ fun ZapDonationButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = Size35dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
animationSize: Dp = 14.dp,
|
||||
animationModifier: Modifier = Size14Modifier,
|
||||
nav: INav,
|
||||
) {
|
||||
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
|
||||
@@ -300,6 +303,8 @@ fun ZapDonationButton(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var zapStartingTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
var hasZapped by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
@@ -308,6 +313,7 @@ fun ZapDonationButton(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onZappingProgress = { progress: Float ->
|
||||
scope.launch { zappingProgress = progress }
|
||||
},
|
||||
@@ -329,6 +335,7 @@ fun ZapDonationButton(
|
||||
zapAmountChoices = it,
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onDismiss = {
|
||||
wantsToZap = null
|
||||
zappingProgress = 0f
|
||||
@@ -376,17 +383,30 @@ fun ZapDonationButton(
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
Spacer(ModifierWidth3dp)
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress =
|
||||
animateFloatAsState(
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = zappingProgress,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
label = "ZapIconIndicator",
|
||||
).value,
|
||||
modifier = remember { Modifier.size(animationSize) },
|
||||
)
|
||||
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
zapStartingTime,
|
||||
) { wasZappedByLoggedInUser ->
|
||||
CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
ZappedIcon(iconSizeModifier)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = animationModifier,
|
||||
strokeWidth = 2.dp,
|
||||
color = grayTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
@@ -423,6 +443,7 @@ fun customZapClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
context: Context,
|
||||
onZapStarts: () -> Unit,
|
||||
onZappingProgress: (Float) -> Unit,
|
||||
onMultipleChoices: (List<Long>) -> Unit,
|
||||
onError: (String, String, User?) -> Unit,
|
||||
@@ -452,6 +473,7 @@ fun customZapClick(
|
||||
val amount = choices.first()
|
||||
|
||||
if (amount > 1100) {
|
||||
onZapStarts()
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amount * 1000,
|
||||
|
||||
+8
-7
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
@@ -202,7 +202,7 @@ open class CommentPostViewModel :
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
@@ -324,13 +324,14 @@ open class CommentPostViewModel :
|
||||
if (message.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraft(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
||||
|
||||
val attachments = mutableSetOf<Event>()
|
||||
nip95attachments.forEach {
|
||||
account.sendToPrivateOutboxAndLocal(it.first)
|
||||
account.sendToPrivateOutboxAndLocal(it.second)
|
||||
attachments.add(it.first)
|
||||
attachments.add(it.second)
|
||||
}
|
||||
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
@@ -50,7 +52,7 @@ fun RefresheableFeedView(
|
||||
nav: INav,
|
||||
) {
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
SaveableFeedState(viewModel, scrollStateKey) { listState ->
|
||||
SaveableFeedState(viewModel.feedState, scrollStateKey) { listState ->
|
||||
RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +60,7 @@ fun RefresheableFeedView(
|
||||
|
||||
@Composable
|
||||
fun SaveableFeedState(
|
||||
viewModel: FeedViewModel,
|
||||
feedContentState: FeedContentState,
|
||||
scrollStateKey: String? = null,
|
||||
content: @Composable (LazyListState) -> Unit,
|
||||
) {
|
||||
@@ -69,7 +71,7 @@ fun SaveableFeedState(
|
||||
rememberLazyListState()
|
||||
}
|
||||
|
||||
WatchScrollToTop(viewModel.feedState, listState)
|
||||
WatchScrollToTop(feedContentState, listState)
|
||||
|
||||
content(listState)
|
||||
}
|
||||
|
||||
-16
@@ -100,22 +100,6 @@ class AccountFeedContentStates(
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
homeNewThreads.destroy()
|
||||
homeReplies.destroy()
|
||||
|
||||
dmKnown.destroy()
|
||||
dmNew.destroy()
|
||||
|
||||
videoFeed.destroy()
|
||||
|
||||
discoverMarketplace.destroy()
|
||||
discoverFollowSets.destroy()
|
||||
discoverReads.destroy()
|
||||
discoverDVMs.destroy()
|
||||
discoverLive.destroy()
|
||||
discoverCommunities.destroy()
|
||||
discoverPublicChats.destroy()
|
||||
|
||||
notifications.destroy()
|
||||
notificationSummary.destroy()
|
||||
|
||||
|
||||
+8
-9
@@ -115,7 +115,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
@@ -412,11 +412,10 @@ class AccountViewModel(
|
||||
|
||||
suspend fun calculateIfNoteWasZappedByAccount(
|
||||
zappedNote: Note,
|
||||
onWasZapped: (Boolean) -> Unit,
|
||||
) {
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote) { onWasZapped(true) }
|
||||
}
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds)
|
||||
}
|
||||
|
||||
suspend fun calculateZapAmount(zappedNote: Note): String =
|
||||
@@ -675,7 +674,7 @@ class AccountViewModel(
|
||||
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
||||
okHttpClient = ::okHttpClientForMoney,
|
||||
onError = onError,
|
||||
onProgress = { onProgress(it) },
|
||||
onProgress = onProgress,
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
zapType = zapType ?: defaultZapType(),
|
||||
)
|
||||
@@ -1528,7 +1527,7 @@ class AccountViewModel(
|
||||
account.deleteDraft(draftTag)
|
||||
}
|
||||
|
||||
suspend fun createTempDraftNote(noteEvent: DraftEvent): Note? = draftNoteCache.update(noteEvent)
|
||||
suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent)
|
||||
|
||||
fun createTempDraftNote(
|
||||
innerEvent: Event,
|
||||
@@ -1709,8 +1708,8 @@ class AccountViewModel(
|
||||
|
||||
class CachedDraftNotes(
|
||||
val accountViewModel: AccountViewModel,
|
||||
) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
|
||||
override suspend fun compute(key: DraftEvent): Note? =
|
||||
) : GenericBaseCacheAsync<DraftWrapEvent, Note>(20) {
|
||||
override suspend fun compute(key: DraftWrapEvent): Note? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key)
|
||||
if (decrypted != null) {
|
||||
|
||||
+6
-6
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.IEvent
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
@@ -74,7 +74,7 @@ class EventProcessor(
|
||||
when (event) {
|
||||
is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote)
|
||||
is OtsEvent -> otsHandler.add(event, eventNote, publicNote)
|
||||
is DraftEvent -> draftHandler.add(event, eventNote, publicNote)
|
||||
is DraftWrapEvent -> draftHandler.add(event, eventNote, publicNote)
|
||||
is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote)
|
||||
is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote)
|
||||
is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote)
|
||||
@@ -98,7 +98,7 @@ class EventProcessor(
|
||||
when (event) {
|
||||
is ChatroomKeyable -> chatHandler.delete(event, note)
|
||||
is OtsEvent -> otsHandler.delete(event, note)
|
||||
is DraftEvent -> draftHandler.delete(event, note)
|
||||
is DraftWrapEvent -> draftHandler.delete(event, note)
|
||||
is GiftWrapEvent -> giftWrapHandler.delete(event, note)
|
||||
is SealedRumorEvent -> sealHandler.delete(event, note)
|
||||
is LnZapRequestEvent -> zapRequest.delete(event, note)
|
||||
@@ -129,7 +129,7 @@ class EventProcessor(
|
||||
val deletedDrafts =
|
||||
newNotes.mapNotNull { note ->
|
||||
val event = note.event
|
||||
if (event is DraftEvent &&
|
||||
if (event is DraftWrapEvent &&
|
||||
event.isDeleted() &&
|
||||
!cache.deletionIndex.hasBeenDeleted(event)
|
||||
) {
|
||||
@@ -193,9 +193,9 @@ class OtsEventHandler(
|
||||
class DraftEventHandler(
|
||||
private val account: Account,
|
||||
private val cache: LocalCache,
|
||||
) : EventHandler<DraftEvent> {
|
||||
) : EventHandler<DraftWrapEvent> {
|
||||
override suspend fun add(
|
||||
event: DraftEvent,
|
||||
event: DraftWrapEvent,
|
||||
eventNote: Note,
|
||||
publicNote: Note,
|
||||
) {
|
||||
|
||||
+10
-14
@@ -32,22 +32,21 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
|
||||
@Composable
|
||||
fun RefreshingChatroomFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
feedContentState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
routeForLastRead: String,
|
||||
@@ -55,12 +54,10 @@ fun RefreshingChatroomFeedView(
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
avoidDraft: DraftTagState? = null,
|
||||
scrollStateKey: String? = null,
|
||||
enablePullRefresh: Boolean = true,
|
||||
) {
|
||||
RefresheableBox(viewModel, enablePullRefresh) {
|
||||
SaveableFeedState(viewModel, scrollStateKey) { listState ->
|
||||
SaveableFeedState(feedContentState, scrollStateKey) { listState ->
|
||||
RenderChatFeedView(
|
||||
viewModel,
|
||||
feedContentState,
|
||||
accountViewModel,
|
||||
listState,
|
||||
nav,
|
||||
@@ -70,12 +67,11 @@ fun RefreshingChatroomFeedView(
|
||||
avoidDraft,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderChatFeedView(
|
||||
viewModel: FeedViewModel,
|
||||
feed: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
listState: LazyListState,
|
||||
nav: INav,
|
||||
@@ -84,13 +80,13 @@ fun RenderChatFeedView(
|
||||
onWantsToEditDraft: (Note) -> Unit,
|
||||
avoidDraft: DraftTagState? = null,
|
||||
) {
|
||||
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||
val feedState by feed.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state ->
|
||||
when (state) {
|
||||
is FeedState.Loading -> LoadingFeed()
|
||||
is FeedState.Empty -> FeedEmpty { viewModel.invalidateData() }
|
||||
is FeedState.FeedError -> FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
is FeedState.Empty -> FeedEmpty { feed.invalidateData() }
|
||||
is FeedState.FeedError -> FeedError(state.errorMessage) { feed.invalidateData() }
|
||||
is FeedState.Loaded ->
|
||||
ChatFeedLoaded(
|
||||
state,
|
||||
@@ -133,7 +129,7 @@ fun ChatFeedLoaded(
|
||||
) {
|
||||
itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item ->
|
||||
val noteEvent = item.event
|
||||
if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() !in avoidDraft.usedDraftTags) {
|
||||
if (avoidDraft == null || noteEvent !is DraftWrapEvent || noteEvent.dTag() !in avoidDraft.usedDraftTags) {
|
||||
ChatroomMessageCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
|
||||
+4
-4
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
|
||||
@@ -162,7 +162,7 @@ fun NormalChatNote(
|
||||
|
||||
ChatBubbleLayout(
|
||||
isLoggedInUser = isLoggedInUser,
|
||||
isDraft = note.event is DraftEvent,
|
||||
isDraft = note.event is DraftWrapEvent,
|
||||
innerQuote = innerQuote,
|
||||
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
|
||||
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
||||
@@ -289,7 +289,7 @@ private fun MessageBubbleLines(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (baseNote.event !is DraftEvent) {
|
||||
if (baseNote.event !is DraftWrapEvent) {
|
||||
RenderReplyRow(
|
||||
note = baseNote,
|
||||
innerQuote = innerQuote,
|
||||
@@ -388,7 +388,7 @@ fun NoteRow(
|
||||
when (note.event) {
|
||||
is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav)
|
||||
is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note, bgColor, accountViewModel, nav)
|
||||
is DraftEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav)
|
||||
is DraftWrapEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav)
|
||||
is ChatMessageEncryptedFileHeaderEvent -> RenderEncryptedFile(note, bgColor, accountViewModel, nav)
|
||||
else -> RenderRegularTextNote(note, canPreview, innerQuote, bgColor, accountViewModel, nav)
|
||||
}
|
||||
|
||||
+2
-2
@@ -132,7 +132,7 @@ fun ChatroomViewUI(
|
||||
.weight(1f, true),
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
feedContentState = feedViewModel.feedState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Room/${room.hashCode()}",
|
||||
@@ -152,7 +152,7 @@ fun ChatroomViewUI(
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
feedViewModel.feedState.sendToTop()
|
||||
}
|
||||
},
|
||||
nav,
|
||||
|
||||
+9
-11
@@ -23,28 +23,26 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
|
||||
class ChatroomFeedFilter(
|
||||
val withUser: ChatroomKey,
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
) : AdditiveFeedFilter<Note>(),
|
||||
ChangesFlowFilter<Note> {
|
||||
fun chatroom() = account.chatroomList.getOrCreatePrivateChatroom(withUser)
|
||||
|
||||
override fun changesFlow() = chatroom().changesFlow()
|
||||
|
||||
// returns the last Note of each user.
|
||||
override fun feedKey(): String = withUser.hashCode().toString()
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser)
|
||||
|
||||
return chatroom.messages
|
||||
.filter { account.isAcceptable(it) }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
override fun feed(): List<Note> = chatroom().messages.filter { account.isAcceptable(it) }.sortedWith(DefaultFeedOrder)
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> {
|
||||
val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser)
|
||||
|
||||
val chatroom = chatroom()
|
||||
return newItems.filter { it in chatroom.messages && account.isAcceptable(it) }.toSet()
|
||||
}
|
||||
|
||||
|
||||
+38
-2
@@ -20,16 +20,25 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ListChange
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChatroomFeedViewModel(
|
||||
val user: ChatroomKey,
|
||||
val account: Account,
|
||||
) : FeedViewModel(ChatroomFeedFilter(user, account)) {
|
||||
) : ListChangeFeedViewModel(ChatroomFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: ChatroomKey,
|
||||
val account: Account,
|
||||
@@ -38,3 +47,30 @@ class ChatroomFeedViewModel(
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = ChatroomFeedViewModel(user, account) as T
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
abstract class ListChangeFeedViewModel(
|
||||
localFilter: ChangesFlowFilter<Note>,
|
||||
) : ViewModel(),
|
||||
InvalidatableContent {
|
||||
val feedState = FeedContentState(localFilter, viewModelScope)
|
||||
|
||||
override val isRefreshing = feedState.isRefreshing
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
|
||||
|
||||
init {
|
||||
Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}")
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
localFilter.changesFlow().collect {
|
||||
Log.d("Init", "Collecting changes to: ${this@ListChangeFeedViewModel.javaClass.simpleName}")
|
||||
when (it) {
|
||||
is ListChange.Addition -> feedState.updateFeedWith(setOf(it.item))
|
||||
is ListChange.Deletion -> feedState.deleteFromFeed(setOf(it.item))
|
||||
is ListChange.SetAddition -> feedState.updateFeedWith(it.item)
|
||||
is ListChange.SetDeletion -> feedState.deleteFromFeed(it.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ fun filterNip04DMs(
|
||||
): List<RelayBasedFilter>? {
|
||||
if (group == null || group.isEmpty() || account == null) return null
|
||||
|
||||
val userOutboxRelays = account.outboxRelays.flow.value
|
||||
val userOutboxRelays = account.homeRelays.flow.value
|
||||
val userInboxRelays = account.dmRelays.flow.value
|
||||
|
||||
val groupOutboxRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ fun NewChatroomSubjectDialog(
|
||||
groupName.value.ifBlank { null }?.let { changeSubject(it) }
|
||||
}
|
||||
|
||||
accountViewModel.account.sendNIP17PrivateMessage(template)
|
||||
accountViewModel.account.sendNip17PrivateMessage(template)
|
||||
}
|
||||
|
||||
onClose()
|
||||
|
||||
+22
-10
@@ -79,7 +79,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
@@ -252,7 +252,7 @@ class ChatNewMessageViewModel :
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
@@ -421,7 +421,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerSendPost(dTag: String?) {
|
||||
private suspend fun innerSendPost(draftTag: String?) {
|
||||
val room = room ?: return
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
@@ -468,19 +468,31 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
accountViewModel.account.sendNIP17PrivateMessage(template, dTag)
|
||||
if (draftTag != null) {
|
||||
accountViewModel.account.createAndSendDraft(draftTag, template)
|
||||
} else {
|
||||
accountViewModel.account.sendPrivateMessage(
|
||||
accountViewModel.account.sendNip17PrivateMessage(template)
|
||||
}
|
||||
} else {
|
||||
val toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
|
||||
|
||||
val template =
|
||||
PrivateDmEvent.build(
|
||||
toUser = toUser,
|
||||
message = message,
|
||||
toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
replyingTo = replyTo.value,
|
||||
contentWarningReason = null,
|
||||
imetas = usedAttachments,
|
||||
draftTag = dTag,
|
||||
replyingTo = replyTo.value?.toEId(),
|
||||
signer = accountViewModel.account.signer,
|
||||
)
|
||||
|
||||
if (draftTag != null) {
|
||||
accountViewModel.account.createAndSendDraft(draftTag, template)
|
||||
} else {
|
||||
accountViewModel.account.sendNip04PrivateMessage(template)
|
||||
}
|
||||
}
|
||||
|
||||
if (dTag == null) {
|
||||
if (draftTag == null) {
|
||||
ChatFileSender(room, accountViewModel.account).sendAll(uploadsWaitingToBeSent)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-10
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||
@@ -48,7 +49,7 @@ class ChatFileSender(
|
||||
contentWarningReason: String?,
|
||||
cipher: AESGCM,
|
||||
) {
|
||||
account.sendNIP17EncryptedFile(
|
||||
account.sendNip17EncryptedFile(
|
||||
ChatMessageEncryptedFileHeaderEvent.build(
|
||||
url = result.url,
|
||||
to = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
@@ -89,18 +90,18 @@ class ChatFileSender(
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
iMetaAttachments.add(result, caption, contentWarningReason)
|
||||
|
||||
account.sendPrivateMessage(
|
||||
val toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
|
||||
|
||||
val template =
|
||||
PrivateDmEvent.build(
|
||||
toUser = toUser,
|
||||
message = result.url,
|
||||
toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
replyingTo = null,
|
||||
zapReceiver = null,
|
||||
contentWarningReason = null,
|
||||
zapRaiserAmount = null,
|
||||
geohash = null,
|
||||
imetas = iMetaAttachments.iMetaAttachments,
|
||||
emojis = null,
|
||||
draftTag = null,
|
||||
replyingTo = null,
|
||||
signer = account.signer,
|
||||
)
|
||||
|
||||
account.sendNip04PrivateMessage(template)
|
||||
}
|
||||
|
||||
suspend fun sendAll(uploads: List<SuccessfulUploads>) {
|
||||
|
||||
+6
-7
@@ -24,21 +24,20 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
|
||||
class ChannelFeedFilter(
|
||||
val channel: Channel,
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
) : AdditiveFeedFilter<Note>(),
|
||||
ChangesFlowFilter<Note> {
|
||||
override fun feedKey() = channel
|
||||
|
||||
override fun changesFlow() = channel.changesFlow()
|
||||
|
||||
// returns the last Note of each user.
|
||||
override fun feed(): List<Note> =
|
||||
sort(
|
||||
channel.notes.filterIntoSet { key, it ->
|
||||
account.isAcceptable(it)
|
||||
},
|
||||
)
|
||||
override fun feed(): List<Note> = sort(channel.notes.filterIntoSet { key, it -> account.isAcceptable(it) })
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> =
|
||||
newItems
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ListChangeFeedViewModel
|
||||
|
||||
class ChannelFeedViewModel(
|
||||
val channel: Channel,
|
||||
val account: Account,
|
||||
) : FeedViewModel(ChannelFeedFilter(channel, account)) {
|
||||
) : ListChangeFeedViewModel(ChannelFeedFilter(channel, account)) {
|
||||
class Factory(
|
||||
val channel: Channel,
|
||||
val account: Account,
|
||||
|
||||
+2
-10
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -41,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.C
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EphemeralChatChannelView(
|
||||
@@ -111,7 +109,7 @@ private fun ChannelView(
|
||||
},
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
feedContentState = feedViewModel.feedState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.roomId.toKey()}",
|
||||
@@ -123,17 +121,11 @@ private fun ChannelView(
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(
|
||||
newPostModel,
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
onSendNewMessage = feedViewModel.feedState::sendToTop,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-10
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -41,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dataso
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun PublicChatChannelView(
|
||||
@@ -111,7 +109,7 @@ fun ChannelView(
|
||||
},
|
||||
) {
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
feedContentState = feedViewModel.feedState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.idHex}",
|
||||
@@ -123,17 +121,11 @@ fun ChannelView(
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(
|
||||
newPostModel,
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
onSendNewMessage = feedViewModel.feedState::sendToTop,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-10
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.C
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LiveActivityChannelView(
|
||||
@@ -113,7 +111,7 @@ fun LiveActivityChannelView(
|
||||
) {
|
||||
ShowVideoStreaming(channel, accountViewModel)
|
||||
RefreshingChatroomFeedView(
|
||||
viewModel = feedViewModel,
|
||||
feedContentState = feedViewModel.feedState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "Channel/${channel.address.toValue()}",
|
||||
@@ -125,17 +123,11 @@ fun LiveActivityChannelView(
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// LAST ROW
|
||||
EditFieldRow(
|
||||
newPostModel,
|
||||
accountViewModel,
|
||||
onSendNewMessage = {
|
||||
scope.launch {
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
},
|
||||
onSendNewMessage = feedViewModel.feedState::sendToTop,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
fun ChannelFileUploadDialog(
|
||||
channelScreenModel: ChannelNewMessageViewModel,
|
||||
state: ChatFileUploadState,
|
||||
onUpload: () -> Unit,
|
||||
onUpload: suspend () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
|
||||
+12
-6
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
@@ -191,7 +191,7 @@ open class ChannelNewMessageViewModel :
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
@@ -259,7 +259,7 @@ open class ChannelNewMessageViewModel :
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
fun sendPost(onDone: () -> Unit) {
|
||||
fun sendPost(onDone: suspend () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
sendPostSync()
|
||||
onDone()
|
||||
@@ -281,8 +281,14 @@ open class ChannelNewMessageViewModel :
|
||||
if (message.text.isBlank()) {
|
||||
account.deleteDraft(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
nip95attachments.forEach {
|
||||
attachments.add(it.first)
|
||||
attachments.add(it.second)
|
||||
}
|
||||
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +299,7 @@ open class ChannelNewMessageViewModel :
|
||||
fun upload(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
onceUploaded: suspend () -> Unit,
|
||||
) = try {
|
||||
uploadUnsafe(onError, context, onceUploaded)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
@@ -306,7 +312,7 @@ open class ChannelNewMessageViewModel :
|
||||
fun uploadUnsafe(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: () -> Unit,
|
||||
onceUploaded: suspend () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val uploadState = uploadState ?: return@launch
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ import kotlinx.coroutines.FlowPreview
|
||||
fun EditFieldRow(
|
||||
channelScreenModel: ChannelNewMessageViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: () -> Unit,
|
||||
onSendNewMessage: suspend () -> Unit,
|
||||
nav: INav,
|
||||
) {
|
||||
channelScreenModel.replyTo.value?.let {
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
|
||||
@Composable
|
||||
fun ChatroomHeaderCompose(
|
||||
@@ -113,7 +113,7 @@ fun ChatroomComposeChannelOrUser(
|
||||
nav: INav,
|
||||
) {
|
||||
val baseNoteEvent = baseNote.event
|
||||
if (baseNoteEvent is DraftEvent) {
|
||||
if (baseNoteEvent is DraftWrapEvent) {
|
||||
ObserveDraftEvent(baseNote, accountViewModel) { innerNote ->
|
||||
ChatroomEntry(innerNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ class ChatroomListKnownFeedFilter(
|
||||
if ((chatroom.senderIntersects(followingKeySet) || chatList.hasSentMessagesTo(key)) &&
|
||||
!account.isAllHidden(key.users)
|
||||
) {
|
||||
chatroom.lastMessage
|
||||
chatroom.newestMessage
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class ChatroomListNewFeedFilter(
|
||||
val privateMessages =
|
||||
chatList.rooms.mapNotNull { key, chatroom ->
|
||||
if (!chatroom.senderIntersects(followingKeySet) && !chatList.hasSentMessagesTo(key) && !account.isAllHidden(key.users)) {
|
||||
chatroom.lastMessage
|
||||
chatroom.newestMessage
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ class DMsFromUserFilterSubAssembler(
|
||||
key: ChatroomListState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? =
|
||||
key.account.outboxRelays.flow.value.map {
|
||||
key.account.homeRelays.flow.value.map {
|
||||
filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
} +
|
||||
key.account.dmRelays.flow.value.map {
|
||||
@@ -58,7 +58,7 @@ class DMsFromUserFilterSubAssembler(
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.Default) {
|
||||
key.account.outboxRelays.flow.collectLatest {
|
||||
key.account.homeRelays.flow.collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
+2
-2
@@ -78,7 +78,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
@@ -192,7 +192,7 @@ open class NewProductViewModel :
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ private fun RenderDraftListScreen(
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
RefresheableBox(feedViewModel) {
|
||||
SaveableFeedState(feedViewModel, DRAFTS) { listState ->
|
||||
SaveableFeedState(feedViewModel.feedState, DRAFTS) { listState ->
|
||||
RenderFeedState(
|
||||
viewModel = feedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
|
||||
class DraftEventsFeedFilter(
|
||||
val account: Account,
|
||||
@@ -48,7 +48,7 @@ class DraftEventsFeedFilter(
|
||||
|
||||
fun acceptableEvent(it: Note): Boolean {
|
||||
val noteEvent = it.event
|
||||
return noteEvent is DraftEvent && noteEvent.pubKey == account.userProfile().pubkeyHex
|
||||
return noteEvent is DraftWrapEvent && noteEvent.pubKey == account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
|
||||
+18
-1
@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -97,6 +98,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -283,7 +285,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
SaveableFeedState(resultFeedViewModel, null) { listState ->
|
||||
SaveableFeedState(resultFeedViewModel.feedState, null) { listState ->
|
||||
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
|
||||
RenderFeedState(
|
||||
resultFeedViewModel,
|
||||
@@ -472,6 +474,7 @@ fun ZapDVMButton(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var zapStartingTime by remember { mutableLongStateOf(0L) }
|
||||
var hasZapped by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
@@ -480,6 +483,7 @@ fun ZapDVMButton(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onZappingProgress = { progress: Float ->
|
||||
scope.launch { zappingProgress = progress }
|
||||
},
|
||||
@@ -501,6 +505,7 @@ fun ZapDVMButton(
|
||||
zapAmountChoices = persistentListOf(amount / 1000),
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onZapStarts = { zapStartingTime = TimeUtils.now() },
|
||||
onDismiss = {
|
||||
wantsToZap = null
|
||||
zappingProgress = 0f
|
||||
@@ -554,12 +559,24 @@ fun ZapDVMButton(
|
||||
label = "ZapIconIndicator",
|
||||
)
|
||||
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
zapStartingTime,
|
||||
) { wasZappedByLoggedInUser ->
|
||||
CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
ZappedIcon(iconSizeModifier)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = remember { Modifier.size(animationSize) },
|
||||
strokeWidth = 2.dp,
|
||||
color = grayTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
|
||||
+8
-9
@@ -96,7 +96,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
@@ -241,7 +241,7 @@ open class ShortNotePostViewModel :
|
||||
val noteEvent = draft?.event
|
||||
val noteAuthor = draft?.author
|
||||
|
||||
if (draft != null && noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
@@ -483,18 +483,17 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
val accountViewModel = accountViewModel
|
||||
|
||||
if (message.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraft(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
||||
|
||||
val attachments = mutableSetOf<Event>()
|
||||
nip95attachments.forEach {
|
||||
account.sendToPrivateOutboxAndLocal(it.first)
|
||||
account.sendToPrivateOutboxAndLocal(it.second)
|
||||
attachments.add(it.first)
|
||||
attachments.add(it.second)
|
||||
}
|
||||
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -40,6 +40,8 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
@@ -129,6 +131,8 @@ class NotificationFeedFilter(
|
||||
it.event !is NIP90ContentDiscoveryRequestEvent &&
|
||||
it.event !is GiftWrapEvent &&
|
||||
it.event !is PrivateTagArrayEvent &&
|
||||
it.event !is LnZapPaymentRequestEvent &&
|
||||
it.event !is LnZapPaymentResponseEvent &&
|
||||
(it.event is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||
(filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) &&
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
|
||||
+8
-9
@@ -85,7 +85,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||
@@ -235,7 +235,7 @@ class NewPublicMessageViewModel :
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
@@ -323,18 +323,17 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
val accountViewModel = accountViewModel
|
||||
|
||||
if (message.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraft(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
||||
|
||||
val broadcast = mutableSetOf<Event>()
|
||||
nip95attachments.forEach {
|
||||
account.sendToPrivateOutboxAndLocal(it.first)
|
||||
account.sendToPrivateOutboxAndLocal(it.second)
|
||||
broadcast.add(it.first)
|
||||
broadcast.add(it.second)
|
||||
}
|
||||
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraft(draftTag.current, template, broadcast)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-6
@@ -56,7 +56,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -97,7 +96,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size8dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -112,15 +111,12 @@ fun ProfileScreen(
|
||||
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(userId) {
|
||||
// waits to resolve.
|
||||
launch(Dispatchers.IO) {
|
||||
val newUserBase = LocalCache.checkGetOrCreateUser(userId)
|
||||
if (newUserBase != userBase) {
|
||||
userBase = newUserBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userBase?.let {
|
||||
PrepareViewModels(
|
||||
@@ -395,7 +391,7 @@ private fun RenderScreen(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
edgePadding = 8.dp,
|
||||
edgePadding = Size8dp,
|
||||
modifier = tabRowModifier,
|
||||
divider = { HorizontalDivider(thickness = DividerThickness) },
|
||||
) {
|
||||
|
||||
+2
-2
@@ -208,7 +208,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
@@ -641,7 +641,7 @@ private fun FullBleedNoteCompose(
|
||||
RenderGitIssueEvent(baseNote, makeItShort = false, canPreview = true, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav)
|
||||
} else if (noteEvent is AppDefinitionEvent) {
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is DraftEvent) {
|
||||
} else if (noteEvent is DraftWrapEvent) {
|
||||
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is HighlightEvent) {
|
||||
RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
|
||||
+1
-2
@@ -93,7 +93,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfFeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
@@ -455,7 +454,7 @@ fun ReactionsColumn(
|
||||
accountViewModel = accountViewModel,
|
||||
iconSize = Size40dp,
|
||||
iconSizeModifier = Size40Modifier,
|
||||
animationSize = Size35dp,
|
||||
animationModifier = Size35Modifier,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ val HalfDoubleVertSpacer = Modifier.height(7.dp)
|
||||
val Size0dp = 0.dp
|
||||
val Size5dp = 5.dp
|
||||
val Size6dp = 6.dp
|
||||
val Size8dp = 8.dp
|
||||
val Size10dp = 10.dp
|
||||
val Size12dp = 12.dp
|
||||
val Size13dp = 13.dp
|
||||
@@ -148,6 +149,7 @@ val MaxWidthWithHorzPadding = Modifier.fillMaxWidth().padding(horizontal = 10.dp
|
||||
|
||||
val Size5Modifier = Modifier.size(5.dp)
|
||||
val Size10Modifier = Modifier.size(10.dp)
|
||||
val Size14Modifier = Modifier.size(14.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
val Size16Modifier = Modifier.size(16.dp)
|
||||
val Size17Modifier = Modifier.size(17.dp)
|
||||
|
||||
@@ -27,7 +27,6 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -55,7 +54,7 @@ class TorService(
|
||||
name: ComponentName,
|
||||
service: IBinder,
|
||||
) {
|
||||
launch {
|
||||
launch(Dispatchers.IO) {
|
||||
// moved torService to a local variable, since we only need it once
|
||||
val torService = (service as LocalBinder).service
|
||||
|
||||
@@ -77,7 +76,6 @@ class TorService(
|
||||
)
|
||||
|
||||
awaitClose {
|
||||
checkNotInMainThread()
|
||||
Log.d("TorService", "Stopping Tor Service")
|
||||
launch {
|
||||
context.stopService(currentIntent)
|
||||
|
||||
@@ -50,6 +50,25 @@ class BundledUpdate(
|
||||
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler)
|
||||
|
||||
val bundledUpdate = BasicBundledUpdate(delay, dispatcher, scope)
|
||||
|
||||
fun invalidate(
|
||||
ignoreIfDoing: Boolean = false,
|
||||
onUpdate: suspend () -> Unit,
|
||||
) = bundledUpdate.invalidate(ignoreIfDoing, onUpdate)
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
@Stable
|
||||
class BasicBundledUpdate(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var invalidatesAgain = false
|
||||
|
||||
@@ -79,10 +98,6 @@ class BundledUpdate(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
@@ -99,6 +114,25 @@ class BundledInsert<T>(
|
||||
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler)
|
||||
|
||||
val bundledInsert = BasicBundledInsert<T>(delay, dispatcher, scope)
|
||||
|
||||
fun invalidateList(
|
||||
newObject: T,
|
||||
onUpdate: suspend (Set<T>) -> Unit,
|
||||
) = bundledInsert.invalidateList(newObject, onUpdate)
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
@Stable
|
||||
class BasicBundledInsert<T>(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var queue = LinkedBlockingQueue<T>()
|
||||
|
||||
@@ -109,32 +143,30 @@ class BundledInsert<T>(
|
||||
checkNotInMainThread()
|
||||
|
||||
queue.put(newObject)
|
||||
|
||||
if (onlyOneInBlock.getAndSet(true)) {
|
||||
// if it was true already, returns.
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch(dispatcher) {
|
||||
try {
|
||||
val mySet = mutableSetOf<T>()
|
||||
queue.drainTo(mySet)
|
||||
if (mySet.isNotEmpty()) {
|
||||
onUpdate(mySet)
|
||||
while (true) {
|
||||
val batch = mutableSetOf<T>()
|
||||
queue.drainTo(batch)
|
||||
if (batch.isNotEmpty()) {
|
||||
onUpdate(batch)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
delay(delay)
|
||||
|
||||
val mySet2 = mutableSetOf<T>()
|
||||
queue.drainTo(mySet2)
|
||||
if (mySet2.isNotEmpty()) {
|
||||
onUpdate(mySet2)
|
||||
}
|
||||
} finally {
|
||||
withContext(NonCancellable) { onlyOneInBlock.set(false) }
|
||||
withContext(NonCancellable) {
|
||||
onlyOneInBlock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ composeBom = "2025.07.00"
|
||||
coreKtx = "1.16.0"
|
||||
datastore = "1.1.7"
|
||||
espressoCore = "3.7.0"
|
||||
firebaseBom = "34.0.0"
|
||||
firebaseBom = "34.1.0"
|
||||
fragmentKtx = "1.8.8"
|
||||
gms = "4.4.3"
|
||||
jacksonModuleKotlin = "2.19.2"
|
||||
@@ -43,7 +43,7 @@ rfc3986 = "0.1.2"
|
||||
secp256k1KmpJniAndroid = "0.18.0"
|
||||
securityCryptoKtx = "1.1.0"
|
||||
spotless = "7.2.1"
|
||||
torAndroid = "0.4.8.17"
|
||||
torAndroid = "0.4.8.17.1"
|
||||
translate = "17.0.3"
|
||||
unifiedpush = "3.0.10"
|
||||
urlDetector = "0.1.23"
|
||||
@@ -111,7 +111,6 @@ jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
|
||||
jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
|
||||
lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" }
|
||||
markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" }
|
||||
|
||||
@@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
@@ -216,7 +216,7 @@ class EventFactory {
|
||||
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
DraftWrapEvent.KIND -> DraftWrapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
@@ -206,7 +206,7 @@ class NostrClient(
|
||||
return false
|
||||
}
|
||||
|
||||
fun sendRequest(
|
||||
fun openReqSubscription(
|
||||
subId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) {
|
||||
@@ -239,7 +239,7 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun sendCount(
|
||||
fun openCountSubscription(
|
||||
subId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) {
|
||||
|
||||
+3
-2
@@ -49,7 +49,6 @@ fun NostrClient.downloadFirstEvent(
|
||||
if (subId == subscriptionId) {
|
||||
unsubscribe(this)
|
||||
close(subscriptionId)
|
||||
|
||||
onResponse(event)
|
||||
}
|
||||
}
|
||||
@@ -57,10 +56,12 @@ fun NostrClient.downloadFirstEvent(
|
||||
|
||||
subscribe(listener)
|
||||
|
||||
sendRequest(subscriptionId, filters)
|
||||
openReqSubscription(subscriptionId, filters)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
delay(30000)
|
||||
|
||||
close(subscriptionId)
|
||||
unsubscribe(listener)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -126,14 +126,14 @@ class SubscriptionController(
|
||||
// was active and is not active anymore, just close.
|
||||
client.close(subId)
|
||||
} else {
|
||||
client.sendRequest(subId, updatedFilters)
|
||||
client.openReqSubscription(subId, updatedFilters)
|
||||
}
|
||||
} else {
|
||||
if (updatedFilters == null) {
|
||||
// was not active and is still not active, does nothing
|
||||
} else {
|
||||
// was not active and becomes active, sends the entire filter.
|
||||
client.sendRequest(subId, updatedFilters)
|
||||
client.openReqSubscription(subId, updatedFilters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ class ReqCmd(
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(msgArray: JsonNode): com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd {
|
||||
fun parse(msgArray: JsonNode): ReqCmd {
|
||||
val filters = mutableListOf<Filter>()
|
||||
|
||||
for (i in 2 until msgArray.size()) {
|
||||
@@ -60,7 +60,7 @@ class ReqCmd(
|
||||
}
|
||||
}
|
||||
|
||||
return com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd(
|
||||
return ReqCmd(
|
||||
msgArray.get(1).asText(),
|
||||
filters,
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
@@ -121,16 +122,37 @@ class PrivateDmEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun build(
|
||||
to: PTag,
|
||||
encryptedMessage: String,
|
||||
suspend fun build(
|
||||
toUser: PTag,
|
||||
message: String,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
replyingTo: EventReference? = null,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
signer: NostrSigner,
|
||||
initializer: TagArrayBuilder<PrivateDmEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, encryptedMessage, createdAt) {
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description =
|
||||
signer.nip04Encrypt(
|
||||
prepareMessageToEncrypt(message, imetas),
|
||||
toUser.pubKey,
|
||||
),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT)
|
||||
pTag(to)
|
||||
pTag(toUser)
|
||||
replyingTo?.let { reply(it) }
|
||||
|
||||
initializer()
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
to: PTag,
|
||||
message: String,
|
||||
imetas: List<IMetaTag>? = null,
|
||||
replyingTo: EventReference?,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
signer: NostrSigner,
|
||||
) = signer.sign(build(to, message, imetas, replyingTo, createdAt, signer))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ class DraftBuilder {
|
||||
draft: T,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
): DraftWrapEvent {
|
||||
val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey)
|
||||
val template =
|
||||
eventTemplate<DraftEvent>(DraftEvent.KIND, encryptedContent, createdAt) {
|
||||
alt(DraftEvent.ALT_DESCRIPTION)
|
||||
eventTemplate<DraftWrapEvent>(DraftWrapEvent.KIND, encryptedContent, createdAt) {
|
||||
alt(DraftWrapEvent.ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
kind(draft.kind)
|
||||
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip37Drafts
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class DraftEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun isDeleted() = content == ""
|
||||
|
||||
fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey
|
||||
|
||||
suspend fun createDeletedEvent(signer: NostrSigner): DraftEvent = signer.sign(createdAt, KIND, tags, "")
|
||||
|
||||
suspend fun decryptInnerEvent(signer: NostrSigner): Event {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
val json = signer.nip44Decrypt(content, pubKey)
|
||||
return try {
|
||||
fromJson(json)
|
||||
} catch (e: JsonParseException) {
|
||||
Log.w("DraftEvent", "Unable to parse inner event of a draft: $json")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 31234
|
||||
const val ALT_DESCRIPTION = "Draft Event"
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
): String = Address.assemble(KIND, pubKey, dTag)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: TorrentCommentEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: InteractiveStoryBaseEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: LiveActivitiesChatMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it, "", "reply")) }
|
||||
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: ChannelMessageEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.channelId()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: GitReplyEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) }
|
||||
originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) }
|
||||
|
||||
return create(dTag, originalNote, tags, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: PollNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: CommentEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies()
|
||||
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
originalNote: TextNoteEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tagsWithMarkers =
|
||||
originalNote.tags.filter {
|
||||
it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply")
|
||||
}
|
||||
|
||||
return create(dTag, originalNote, tagsWithMarkers, signer, createdAt)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
innerEvent: Event,
|
||||
anchorTagArray: List<Array<String>> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftEvent {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
tags.add(arrayOf("d", dTag))
|
||||
tags.add(arrayOf("k", "${innerEvent.kind}"))
|
||||
|
||||
if (anchorTagArray.isNotEmpty()) {
|
||||
tags.addAll(anchorTagArray)
|
||||
}
|
||||
|
||||
val draft =
|
||||
signer.sign<DraftEvent>(
|
||||
createdAt = createdAt,
|
||||
kind = KIND,
|
||||
tags = tags.toTypedArray(),
|
||||
content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey),
|
||||
)
|
||||
|
||||
return draft
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ class DraftEventCache(
|
||||
signer: NostrSigner,
|
||||
) {
|
||||
private val decryptionCache =
|
||||
object : LruCache<DraftEvent, DraftEventDecryptCache>(1000) {
|
||||
override fun create(key: DraftEvent): DraftEventDecryptCache? =
|
||||
object : LruCache<DraftWrapEvent, DraftEventDecryptCache>(1000) {
|
||||
override fun create(key: DraftWrapEvent): DraftEventDecryptCache? =
|
||||
if (!key.isDeleted() && key.pubKey == signer.pubKey) {
|
||||
DraftEventDecryptCache(signer)
|
||||
} else {
|
||||
@@ -38,23 +38,23 @@ class DraftEventCache(
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(event: DraftEvent) = decryptionCache.remove(event)
|
||||
fun delete(event: DraftWrapEvent) = decryptionCache.remove(event)
|
||||
|
||||
fun preload(
|
||||
event: DraftEvent,
|
||||
event: DraftWrapEvent,
|
||||
result: Event,
|
||||
) = decryptionCache[event]?.preload(result)
|
||||
|
||||
fun preCachedDraft(event: DraftEvent): Event? = decryptionCache[event]?.cached()
|
||||
fun preCachedDraft(event: DraftWrapEvent): Event? = decryptionCache[event]?.cached()
|
||||
|
||||
suspend fun cachedDraft(event: DraftEvent) = decryptionCache[event]?.decrypt(event)
|
||||
suspend fun cachedDraft(event: DraftWrapEvent) = decryptionCache[event]?.decrypt(event)
|
||||
}
|
||||
|
||||
class DraftEventDecryptCache(
|
||||
signer: NostrSigner,
|
||||
) : DecryptCache<DraftEvent, Event>(signer) {
|
||||
) : DecryptCache<DraftWrapEvent, Event>(signer) {
|
||||
override suspend fun decryptAndParse(
|
||||
event: DraftEvent,
|
||||
event: DraftWrapEvent,
|
||||
signer: NostrSigner,
|
||||
): Event = event.decryptInnerEvent(signer)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip37Drafts
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class DraftWrapEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun isDeleted() = content == ""
|
||||
|
||||
fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey
|
||||
|
||||
suspend fun decryptInnerEvent(signer: NostrSigner): Event {
|
||||
if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
val json = signer.nip44Decrypt(content, pubKey)
|
||||
return try {
|
||||
fromJson(json)
|
||||
} catch (e: JsonParseException) {
|
||||
Log.w("DraftEvent", "Unable to parse inner event of a draft: $json")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 31234
|
||||
const val ALT_DESCRIPTION = "Draft Event"
|
||||
|
||||
fun createAddress(
|
||||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
): Address = Address(KIND, pubKey, dTag)
|
||||
|
||||
fun createAddressTag(
|
||||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
): String = Address.assemble(KIND, pubKey, dTag)
|
||||
|
||||
suspend fun build(
|
||||
dTag: String,
|
||||
draft: Event,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<DraftWrapEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description = signer.nip44Encrypt(draft.toJson(), signer.pubKey),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
kind(draft.kind)
|
||||
|
||||
initializer()
|
||||
}
|
||||
|
||||
suspend fun buildDeleted(
|
||||
dTag: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<DraftWrapEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description = "",
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
dTag(dTag)
|
||||
initializer()
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
dTag: String,
|
||||
draft: Event,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = signer.sign(build(dTag, draft, signer, createdAt))
|
||||
|
||||
suspend fun createDeletedEvent(
|
||||
dTag: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): DraftWrapEvent = signer.sign(buildDeleted(dTag, createdAt))
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -76,8 +76,11 @@ class AdvertisedRelayInfo(
|
||||
|
||||
@JvmStatic
|
||||
fun parseReadNorm(tag: Array<String>): NormalizedRelayUrl? {
|
||||
ensure(match(tag)) { return null }
|
||||
ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null }
|
||||
ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null }
|
||||
|
||||
if (tag.has(2)) {
|
||||
ensure(AdvertisedRelayType.isRead(tag[2])) { return null }
|
||||
}
|
||||
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(tag[1])
|
||||
|
||||
@@ -88,8 +91,11 @@ class AdvertisedRelayInfo(
|
||||
|
||||
@JvmStatic
|
||||
fun parseWriteNorm(tag: Array<String>): NormalizedRelayUrl? {
|
||||
ensure(match(tag)) { return null }
|
||||
ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null }
|
||||
ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null }
|
||||
|
||||
if (tag.has(2)) {
|
||||
ensure(AdvertisedRelayType.isWrite(tag[2])) { return null }
|
||||
}
|
||||
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(tag[1])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user