From cdfb1db253e99edbe39976913739ed6c7e096bde Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 2 Aug 2025 16:02:50 +0200 Subject: [PATCH 1/5] refactor DecryptAndIndexProcessor EventProcessor: Orchestrates the processing flow DecryptionService: Handles all decryption operations IndexingService: Manages event indexing and relationships ChatroomService: Manages chatroom operations --- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../loggedIn/DecryptAndIndexProcessor.kt | 637 ++++++++++++------ 2 files changed, 421 insertions(+), 218 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 3f5079dfc..93885c30b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -173,7 +173,7 @@ class AccountViewModel( scope = viewModelScope, ) - val newNotesPreProcessor = DecryptAndIndexProcessor(account, LocalCache) + val newNotesPreProcessor = EventProcessor(account, account.cache) var firstRoute: Route? = null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index b7ef28cb1..d74757b52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent 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.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -39,241 +40,180 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException +import kotlin.reflect.KClass -class DecryptAndIndexProcessor( - val account: Account, - val cache: LocalCache, +class EventProcessor( + private val account: Account, + private val cache: LocalCache, ) { + private val decryptionService = DecryptionService(account, cache) + private val indexingService = IndexingService(account, cache) + private val chatroomService = ChatroomService(account) + private val eventHandlers = createEventHandlers() + suspend fun consume(note: Note) { - val noteEvent = note.event - if (noteEvent != null) { + note.event?.let { event -> try { - consumeAlreadyVerified(noteEvent, note, note) + processEvent(event, note, note) } catch (e: Exception) { - Log.e("PrecacheNewNotesProcessor", "Error processing note", e) - } - } - } - - suspend fun consumeAlreadyVerified( - event: Event, - eventNote: Note, - publicNote: Note, - ) { - when (event) { - is PrivateDmEvent -> { - if (event.canDecrypt(account.signer.pubKey)) { - val talkingWith = event.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(talkingWith, eventNote) - } - } - - is ChatMessageEvent -> { - if (event.isIncluded(account.signer.pubKey)) { - val key = event.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, eventNote) - } - } - - is ChatMessageEncryptedFileHeaderEvent -> { - if (event.isIncluded(account.signer.pubKey)) { - val key = event.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, eventNote) - } - } - - is OtsEvent -> { - // verifies new OTS upon arrival - Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) - } - - is DraftEvent -> { - // Avoid decrypting over and over again if the event already exist. - if (event.pubKey == account.signer.pubKey) { - if (!event.isDeleted()) { - val rumor = account.draftsDecryptionCache.preCachedDraft(event) ?: account.draftsDecryptionCache.cachedDraft(event) - if (rumor != null) { - indexDraftAsRealEvent(eventNote, rumor) - } - } - } - } - - is GiftWrapEvent -> { - if (event.recipientPubKey() == account.signer.pubKey) { - val innerGiftId = event.innerEventId - if (innerGiftId == null) { - event.unwrapOrNull(account.signer)?.let { innerGift -> - // clear the encrypted payload to save memory - eventNote.event = event.copyNoContent() - if (cache.justConsume(innerGift, null, false)) { - cache.copyRelaysFromTo(publicNote, innerGift) - val innerGiftNote = cache.getOrCreateNote(innerGift.id) - consumeAlreadyVerified(innerGift, innerGiftNote, publicNote) - } - } - } else { - cache.copyRelaysFromTo(publicNote, innerGiftId) - - val innerGiftNote = cache.getOrCreateNote(innerGiftId) - val innerGift = innerGiftNote.event - if (innerGift != null) { - consumeAlreadyVerified(innerGift, innerGiftNote, publicNote) - } - } - } - } - - is SealedRumorEvent -> { - val rumorId = event.innerEventId - if (rumorId == null) { - event.unsealOrNull(account.signer)?.let { innerRumor -> - // clear the encrypted payload to save memory - eventNote.event = event.copyNoContent() - - // rumors cannot be verified - cache.justConsume(innerRumor, null, true) - - cache.copyRelaysFromTo(publicNote, innerRumor) - val innerRumorNote = cache.getOrCreateNote(innerRumor.id) - consumeAlreadyVerified(innerRumor, innerRumorNote, publicNote) - } - } else { - cache.copyRelaysFromTo(publicNote, rumorId) - - val innerRumorNote = cache.getOrCreateNote(rumorId) - val innerRumor = innerRumorNote.event - if (innerRumor != null) { - consumeAlreadyVerified(innerRumor, innerRumorNote, publicNote) - } - } - } - - is LnZapRequestEvent -> { - if (account.privateZapsDecryptionCache.cachedPrivateZap(event) == null && event.isPrivateZap()) { - account.privateZapsDecryptionCache.decryptPrivateZap(event) - } + Log.e("EventProcessor", "Error processing note", e) } } } suspend fun delete(note: Note) { - val noteEvent = note.event - if (noteEvent != null) { + note.event?.let { event -> try { - delete(noteEvent, note) + deleteEvent(event, note) } catch (e: Exception) { - Log.e("PrecacheNewNotesProcessor", "Error processing note", e) + Log.e("EventProcessor", "Error deleting note", e) } } } - suspend fun delete( + internal suspend fun processEvent( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + eventHandlers[event::class]?.process(event, eventNote, publicNote) + } + + internal suspend fun deleteEvent( event: Event, eventNote: Note, ) { - when (event) { - is PrivateDmEvent -> { - if (event.canDecrypt(account.signer.pubKey)) { - // Avoid decrypting over and over again if the event already exist. - val talkingWith = event.chatroomKey(account.signer.pubKey) - account.chatroomList.removeMessage(talkingWith, eventNote) - } - } + eventHandlers[event::class]?.delete(event, eventNote) + } - is ChatMessageEvent -> { - if (event.isIncluded(account.signer.pubKey)) { - val key = event.chatroomKey(account.signer.pubKey) - account.chatroomList.removeMessage(key, eventNote) - } - } + private fun createEventHandlers(): Map, EventHandler> = + mapOf( + PrivateDmEvent::class to PrivateDmHandler(chatroomService), + ChatMessageEvent::class to ChatMessageHandler(chatroomService), + ChatMessageEncryptedFileHeaderEvent::class to ChatMessageEncryptedFileHeaderHandler(chatroomService), + OtsEvent::class to OtsEventHandler(account), + DraftEvent::class to DraftEventHandler(decryptionService, indexingService), + GiftWrapEvent::class to GiftWrapEventHandler(decryptionService, cache, this), + SealedRumorEvent::class to SealedRumorEventHandler(decryptionService, cache, this), + LnZapRequestEvent::class to LnZapRequestEventHandler(decryptionService), + LnZapEvent::class to LnZapEventHandler(decryptionService), + ) - is ChatMessageEncryptedFileHeaderEvent -> { - if (event.isIncluded(account.signer.pubKey)) { - val key = event.chatroomKey(account.signer.pubKey) - account.chatroomList.removeMessage(key, eventNote) - } - } - - is DraftEvent -> { - // Avoid decrypting over and over again if the event already exist. - if (event.pubKey == account.signer.pubKey) { - // already deindexed by LocalCache - // deindexDraftAsRealEvent(eventNote) - } - } - - is GiftWrapEvent -> { - if (event.recipientPubKey() == account.signer.pubKey) { - val innerGiftId = event.innerEventId - if (innerGiftId != null) { - val innerGiftNote = cache.getNoteIfExists(innerGiftId) - val innerGift = innerGiftNote?.event - if (innerGift != null) { - delete(innerGift, innerGiftNote) - } - } - } - } - - is SealedRumorEvent -> { - val rumorId = event.innerEventId - if (rumorId != null) { - val innerRumorNote = cache.getNoteIfExists(rumorId) - val innerRumor = innerRumorNote?.event - if (innerRumor != null) { - delete(innerRumor, innerRumorNote) - } - } - } - - is LnZapEvent -> { - event.zapRequest?.let { req -> - if (req.isPrivateZap()) { - // We can't know which account this was for without going through it. - account.privateZapsDecryptionCache.delete(req) - } - } - } - - is LnZapRequestEvent -> { - if (event.isPrivateZap()) { - account.privateZapsDecryptionCache.delete(event) - } - } - // .. + suspend fun runNew(newNotes: Set) { + try { + newNotes.forEach { consume(it) } + handleDeletedDrafts(newNotes) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("EventProcessor", "Error processing batch", e) } } + suspend fun runDeleted(notes: Set) { + try { + notes.forEach { delete(it) } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("EventProcessor", "Error deleting batch", e) + } + } + + private suspend fun handleDeletedDrafts(newNotes: Set) { + val deletedDrafts = + newNotes.mapNotNull { note -> + val event = note.event + if (event is DraftEvent && event.isDeleted()) note else null + } + + if (deletedDrafts.isNotEmpty()) { + Log.w("EventProcessor", "Deleting ${deletedDrafts.size} draft notes") + account.delete(deletedDrafts) + } + } +} + +// Service classes following Single Responsibility Principle +class DecryptionService( + val account: Account, + private val cache: LocalCache, +) { + suspend fun unwrapGiftWrap(event: GiftWrapEvent): Event? = event.unwrapOrNull(account.signer) + + suspend fun unsealRumor(event: SealedRumorEvent): Event? = event.unsealOrNull(account.signer) + + suspend fun decryptDraft(event: DraftEvent): Event? = + account.draftsDecryptionCache.preCachedDraft(event) + ?: account.draftsDecryptionCache.cachedDraft(event) + + suspend fun handlePrivateZap(event: LnZapRequestEvent) { + if (account.privateZapsDecryptionCache.cachedPrivateZap(event) == null && event.isPrivateZap()) { + account.privateZapsDecryptionCache.decryptPrivateZap(event) + } + } + + fun deletePrivateZap(event: LnZapRequestEvent) { + if (event.isPrivateZap()) { + account.privateZapsDecryptionCache.delete(event) + } + } + + fun deletePrivateZapFromZapEvent(event: LnZapEvent) { + event.zapRequest?.let { req -> + if (req.isPrivateZap()) { + account.privateZapsDecryptionCache.delete(req) + } + } + } +} + +class IndexingService( + private val account: Account, + private val cache: LocalCache, +) { fun indexDraftAsRealEvent( draftEventWrap: Note, rumor: Event, + ) { + setupReplyRelationships(draftEventWrap, rumor) + indexByEventType(draftEventWrap, rumor) + } + + private fun setupReplyRelationships( + draftEventWrap: Note, + rumor: Event, ) { draftEventWrap.replyTo = cache.computeReplyTo(rumor) draftEventWrap.replyTo?.forEach { it.addReply(draftEventWrap) } + } + + private fun indexByEventType( + draftEventWrap: Note, + rumor: Event, + ) { + val chatroomService = ChatroomService(account) when (rumor) { is PrivateDmEvent -> { if (rumor.canDecrypt(account.signer)) { - val talkingWith = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(talkingWith, draftEventWrap) + val key = rumor.chatroomKey(account.signer.pubKey) + chatroomService.addMessage(key, draftEventWrap) } } is ChatMessageEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, draftEventWrap) + chatroomService.addMessage(key, draftEventWrap) } } is ChatMessageEncryptedFileHeaderEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, draftEventWrap) + chatroomService.addMessage(key, draftEventWrap) } } is EphemeralChatEvent -> { - rumor.roomId()?.let { - val channel = cache.getOrCreateEphemeralChannel(it) + rumor.roomId()?.let { roomId -> + val channel = cache.getOrCreateEphemeralChannel(roomId) channel.addNote(draftEventWrap, null) } } @@ -291,41 +231,304 @@ class DecryptAndIndexProcessor( } } } +} - suspend fun runNew(newNotes: Set) { - try { - newNotes.forEach { - consume(it) - } +class ChatroomService( + val account: Account, +) { + fun addMessage( + chatroomKey: ChatroomKey, + note: Note, + ) { + account.chatroomList.addMessage(chatroomKey, note) + } - val toDelete = - newNotes.mapNotNull { - val noteEvent = it.event - if (noteEvent is DraftEvent && noteEvent.isDeleted()) { - it - } else { - null - } - } + fun removeMessage( + chatroomKey: ChatroomKey, + note: Note, + ) { + account.chatroomList.removeMessage(chatroomKey, note) + } +} - if (toDelete.isNotEmpty()) { - Log.w("PrecacheNewNotesProcessor", "Deleting ${toDelete.size} draft notes that should have been deleted already") - account.delete(toDelete) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) +// Event handler interface and implementations following Strategy Pattern +interface EventHandler { + suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) {} + + suspend fun delete( + event: Event, + eventNote: Note, + ) {} +} + +class PrivateDmHandler( + private val chatroomService: ChatroomService, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as PrivateDmEvent + if (event.canDecrypt(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.addMessage(key, eventNote) } } - suspend fun runDeleted(newNotes: Set) { - try { - newNotes.forEach { - delete(it) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as PrivateDmEvent + if (event.canDecrypt(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.removeMessage(key, eventNote) } } } + +class ChatMessageHandler( + private val chatroomService: ChatroomService, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as ChatMessageEvent + if (event.isIncluded(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.addMessage(key, eventNote) + } + } + + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as ChatMessageEvent + if (event.isIncluded(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.removeMessage(key, eventNote) + } + } +} + +class ChatMessageEncryptedFileHeaderHandler( + private val chatroomService: ChatroomService, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as ChatMessageEncryptedFileHeaderEvent + if (event.isIncluded(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.addMessage(key, eventNote) + } + } + + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as ChatMessageEncryptedFileHeaderEvent + if (event.isIncluded(chatroomService.account.signer.pubKey)) { + val key = event.chatroomKey(chatroomService.account.signer.pubKey) + chatroomService.removeMessage(key, eventNote) + } + } +} + +class OtsEventHandler( + private val account: Account, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as OtsEvent + Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) + } +} + +class DraftEventHandler( + private val decryptionService: DecryptionService, + private val indexingService: IndexingService, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as DraftEvent + if (event.pubKey == decryptionService.account.signer.pubKey && !event.isDeleted()) { + val rumor = decryptionService.decryptDraft(event) + rumor?.let { indexingService.indexDraftAsRealEvent(eventNote, it) } + } + } +} + +class GiftWrapEventHandler( + private val decryptionService: DecryptionService, + private val cache: LocalCache, + private val eventProcessor: EventProcessor, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as GiftWrapEvent + if (event.recipientPubKey() != decryptionService.account.signer.pubKey) return + + val innerGiftId = event.innerEventId + if (innerGiftId == null) { + processNewGiftWrap(event, eventNote, publicNote) + } else { + processExistingGiftWrap(innerGiftId, publicNote) + } + } + + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as GiftWrapEvent + if (event.recipientPubKey() != decryptionService.account.signer.pubKey) return + + event.innerEventId?.let { innerGiftId -> + val innerGiftNote = cache.getNoteIfExists(innerGiftId) + innerGiftNote?.event?.let { innerGift -> + eventProcessor.deleteEvent(innerGift, innerGiftNote) + } + } + } + + private suspend fun processNewGiftWrap( + event: GiftWrapEvent, + eventNote: Note, + publicNote: Note, + ) { + val innerGift = decryptionService.unwrapGiftWrap(event) ?: return + + eventNote.event = event.copyNoContent() + if (cache.justConsume(innerGift, null, false)) { + cache.copyRelaysFromTo(publicNote, innerGift) + val innerGiftNote = cache.getOrCreateNote(innerGift.id) + eventProcessor.processEvent(innerGift, innerGiftNote, publicNote) + } + } + + private suspend fun processExistingGiftWrap( + innerGiftId: String, + publicNote: Note, + ) { + cache.copyRelaysFromTo(publicNote, innerGiftId) + val innerGiftNote = cache.getOrCreateNote(innerGiftId) + innerGiftNote.event?.let { innerGift -> + eventProcessor.processEvent(innerGift, innerGiftNote, publicNote) + } + } +} + +class SealedRumorEventHandler( + private val decryptionService: DecryptionService, + private val cache: LocalCache, + private val eventProcessor: EventProcessor, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as SealedRumorEvent + + val rumorId = event.innerEventId + if (rumorId == null) { + processNewSealedRumor(event, eventNote, publicNote) + } else { + processExistingSealedRumor(rumorId, publicNote) + } + } + + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as SealedRumorEvent + + event.innerEventId?.let { rumorId -> + val innerRumorNote = cache.getNoteIfExists(rumorId) + innerRumorNote?.event?.let { innerRumor -> + eventProcessor.deleteEvent(innerRumor, innerRumorNote) + } + } + } + + private suspend fun processNewSealedRumor( + event: SealedRumorEvent, + eventNote: Note, + publicNote: Note, + ) { + val innerRumor = decryptionService.unsealRumor(event) ?: return + + eventNote.event = event.copyNoContent() + cache.justConsume(innerRumor, null, true) + cache.copyRelaysFromTo(publicNote, innerRumor) + + val innerRumorNote = cache.getOrCreateNote(innerRumor.id) + eventProcessor.processEvent(innerRumor, innerRumorNote, publicNote) + } + + private suspend fun processExistingSealedRumor( + rumorId: String, + publicNote: Note, + ) { + cache.copyRelaysFromTo(publicNote, rumorId) + val innerRumorNote = cache.getOrCreateNote(rumorId) + innerRumorNote.event?.let { innerRumor -> + eventProcessor.processEvent(innerRumor, innerRumorNote, publicNote) + } + } +} + +class LnZapRequestEventHandler( + private val decryptionService: DecryptionService, +) : EventHandler { + override suspend fun process( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + event as LnZapRequestEvent + decryptionService.handlePrivateZap(event) + } + + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as LnZapRequestEvent + decryptionService.deletePrivateZap(event) + } +} + +class LnZapEventHandler( + private val decryptionService: DecryptionService, +) : EventHandler { + override suspend fun delete( + event: Event, + eventNote: Note, + ) { + event as LnZapEvent + decryptionService.deletePrivateZapFromZapEvent(event) + } +} From fa80cb297d6f50ae96fbea4d7504dfb4c0c5f6ce Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 3 Aug 2025 10:24:05 +0200 Subject: [PATCH 2/5] reduce lint warnings --- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 12 ++++++------ .../ui/screen/loggedIn/DecryptAndIndexProcessor.kt | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 93885c30b..47b886e32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -949,7 +949,7 @@ class AccountViewModel( } } - suspend fun loadReactionTo(note: Note?): String? { + fun loadReactionTo(note: Note?): String? { if (note == null) return null return note.getReactionBy(userProfile()) @@ -1027,7 +1027,7 @@ class AccountViewModel( fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex) - private suspend fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key) + private fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key) override suspend fun getOrCreateNote(key: HexKey): Note = LocalCache.getOrCreateNote(key) @@ -1102,11 +1102,11 @@ class AccountViewModel( ) } - suspend fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key) + fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key) - suspend fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key) + fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key) - suspend fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key) + fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key) fun checkGetOrCreateChannel( key: HexKey, @@ -1660,7 +1660,7 @@ class AccountViewModel( } } - suspend fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account) + fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account) fun relayStatusFlow() = app.client.relayStatusFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index d74757b52..7bca57ce9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -46,7 +46,7 @@ class EventProcessor( private val account: Account, private val cache: LocalCache, ) { - private val decryptionService = DecryptionService(account, cache) + private val decryptionService = DecryptionService(account) private val indexingService = IndexingService(account, cache) private val chatroomService = ChatroomService(account) private val eventHandlers = createEventHandlers() @@ -132,10 +132,8 @@ class EventProcessor( } } -// Service classes following Single Responsibility Principle class DecryptionService( val account: Account, - private val cache: LocalCache, ) { suspend fun unwrapGiftWrap(event: GiftWrapEvent): Event? = event.unwrapOrNull(account.signer) @@ -251,7 +249,6 @@ class ChatroomService( } } -// Event handler interface and implementations following Strategy Pattern interface EventHandler { suspend fun process( event: Event, From 5420ddd338bc61c2cac20d7fa9b77fc4fa6e023f Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 3 Aug 2025 10:35:40 +0200 Subject: [PATCH 3/5] lets pass LocalCache instead of account.cache, as it was before --- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 47b886e32..39dd1126f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -173,7 +173,7 @@ class AccountViewModel( scope = viewModelScope, ) - val newNotesPreProcessor = EventProcessor(account, account.cache) + val newNotesPreProcessor = EventProcessor(account, LocalCache) var firstRoute: Route? = null From bef607ec411ec890c18e963519e3687a05808cef Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 4 Aug 2025 15:14:07 +0000 Subject: [PATCH 4/5] New Crowdin translations by GitHub Action --- .../src/main/res/values-zh-rCN/strings.xml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index f54085bcf..2ff9f4e65 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -51,6 +51,13 @@ 你正在使用公钥,公钥是只读的。使用私钥登录以便能够取消关注 你正在使用公钥,公钥是只读的。使用私钥登录以便能够隐藏单词或句子 你正在使用公钥,公钥是只读的。使用私钥登录以便能够显示单词或句子 + 你正在使用公钥,公钥是只读的。要更改设置请用私钥登录 + 你正在使用公钥,公钥是只读的。要上传请使用私钥登录 + 你正在使用公钥,公钥是只读的。要报名参加活动请使用私钥登录 + 未授权的解密 + 签名人没有授权进行此操作所需的解密。在签名人应用中激活 NIP-44 解密并重试 + 未找到签名人 + 签名人应用被卸载了吗?检查是否安装了签名人以及是否签名人有该账户。注销并再次登录,签名人应用已更改。 打闪 浏览次数 提升 @@ -98,6 +105,7 @@ 我的精彩群聊 图片链接 描述 + 未找到描述 "关于我们.. " 你在想什么? 写一条消息… @@ -139,6 +147,10 @@ 视频已保存到媒体库 保存视频失败 上传图片 + 拍照 + 录制消息 + 录制消息 + 单击并按住来录制消息 上传中… 用户尚未设置闪电地址以接收聪 "🔏在此回复… " @@ -231,6 +243,8 @@ 中继器 使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。 + 付费中继 + 连接时强制使用 Tor 收到文章 移除 自动 @@ -275,6 +289,7 @@ 错误 "由 %1$s 创建" "颁发给 %1$s 的徽章图片" + \"徽章奖品图片 你收到了新的徽章奖励 徽章奖励授予 文本已复制到剪贴板 @@ -422,6 +437,7 @@ 关注列表 所有关注 + 通过代理关注 周围的人 全球 静音列表 @@ -693,6 +709,7 @@ 钱包 %1$s 打开签名应用时出错 找不到签名应用。检查应用是否已被卸载 + 签名请求被拒绝了 签名请求被拒绝了 请确保签名应用程序已授权此交易 找不到支付闪电发票的钱包(错误:%1$s)。请安装闪电钱包来使用打闪 @@ -876,12 +893,29 @@ 设置 1 ~ 3 个中继用于保存其他人无法看到的事件,例如您的草稿和软件设置。理想情况下,这些应该是本地中继,或者是在获取用户内容时需要经过身份验证的中继。 通用中继 Amethyst 会通过这些中继为您获取帖子。 + 已连接的中继 + 当前正在使用的中继列表 推荐的中继器 在通用中继列表中添加下列中继以接收列表中用户的帖子。 搜索中继 用于关键词和标签检索的中继列表。如果没有可用选项,也就无法使用关键词和标签检索。需要确保它们支持 NIP-50。 本地中继 在该设备中运行的中继列表。 + 可信中继 + 可信中继 + 你信任的无需 Tor 连接的中继 + 代理中继 + 代理中继 + 聚合器中继下载 feed 必须使用的应用的流量,类似 filter.nostr.wine。这替代 outbox 模型,让应用只连接到列表中的中继。 + 广播中继 + 广播中继 + 专门推送便笩到所有其他中继的中继,类似 sendit.nosflare.com。Amethyst 会将这个中继添加到所有你所做的新事件 + 索引器中继 + 索引器中继 + 专门托管每个人的元数据和中继列表的中继,类似 purplepag.es。Amethyst将使用这些中继来查找不在列表中的用户。 + 屏蔽的中继 + 屏蔽的中继 + Amethyst 永远不会连接到这些中继 打闪开发人员! 你的捐赠帮助我们做出不同的贡献。每个聪都很重要! 立即捐款 @@ -971,6 +1005,7 @@ 选择一个用于过滤订阅源的列表 当设备锁定时注销 私信 + 公开消息 聊天中继 此聊天所有用户都连接到的中继 分享图片… From f9efc0ae2d90f87a11c147fe63b162b185bc86bc Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 5 Aug 2025 13:51:50 +0000 Subject: [PATCH 5/5] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index bbbdcc944..94d56519b 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -50,7 +50,14 @@ Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc obserwować Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc przestać obserwować Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc ukryć słowo lub zdanie - Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc pokazać słowo lub zdanie + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc pokazać wyraz lub zdanie + Używasz klucza publicznego i kluczy publicznych tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc zmieniać ustawienia + Używasz klucza publicznego i kluczy publicznych tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc wgrać + Używasz klucza publicznego i kluczy publicznych tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc zapisać się na udział w wydarzeniu + Nieautoryzowane odszyfrowywanie + Sygnatariusz nie autoryzował deszyfrowania wymaganego do wykonania tej operacji. Aktywuj deszyfrowanie NIP-44 w aplikacji podpisującej i spróbuj ponownie + Nie odnaleziono sygnatariusza + Czy aplikacja sygnatariusza została odinstalowana? Sprawdź, czy aplikacja sygnatariusza jest zainstalowana i czy ma to konto. Wyloguj się i zaloguj ponownie, jeśli aplikacja sygnatariusza uległa zmianie. Zapy Liczba wyświetleń Promuj @@ -98,6 +105,7 @@ Nazwa Grupy Adres Url obrazka Opis + Nie znaleziono opisu "Informacja o grupie…" Co masz na myśli? Napisz wiadomość… @@ -139,6 +147,10 @@ Film zapisany w galerii filmów Nie udało się zapisać filmu Dodaj zdjęcie + Zrób zdjęcie + Nagraj wiadomość + Nagrywanie wiadomości + Kliknij i przytrzymaj aby nagrać wiadomość Wgrywanie… Użytkownik nie ma skonfigurowanego adresu LN, aby odbierać satsy "odpowiedz tutaj.. " @@ -228,6 +240,8 @@ Czaty publiczne są widoczne dla wszystkich użytkowników Nostr i każdy może w nich uczestniczyć. Są idealne dla otwartych społeczności skupionych wokół konkretnych tematów. Moderację można kontrolować, usuwając posty z transmiterów Transmitery Wstaw od 1 do 3 transmiterów, które obsługują tę grupę. Klienci Nostr używają tego ustawienia, aby wiedzieć, skąd pobierać wiadomości i do kogo je wysyłać. + Płatny transmiter + Wymusza Tor podczas łączenia odebranych wiadomości Usuń Automatycznie @@ -272,6 +286,7 @@ Błąd "Utworzony przez %1$s" "Wizerunek odznaki dla %1$s" + Wizerunek odznaki Otrzymałeś nową odznakę Nagroda przyznana dla Skopiowano tekst wpisu do schowka @@ -419,6 +434,7 @@ Nie Lista obserwowanych Obserwowane + Obserwuje przez proxy W pobliżu Wszystkie Zablokowane @@ -690,6 +706,7 @@ Portfel %1$s Błąd podczas otwierania aplikacji podpisującej Nie można odnaleźć aplikacji podpisującej. Sprawdź, czy aplikacja nie została odinstalowana + Prośba o zalogowanie została odrzucona Odrzucono aplikację podpisującego Upewnij się, że aplikacja podpisującego autoryzuje tę transakcję Nie znaleziono portfeli do zapłacenia faktury z Lightning (Error: %1$s). Proszę zainstalować Lightning wallet, aby używać zapów @@ -873,12 +890,29 @@ Wstaw od 1 do 3 transmiterów przechowujących dane zdarzeń, których nikt inny nie widzi, takich jak Wersje robocze i/lub ustawienia aplikacji. Idealnie byłoby, gdyby te transmitery były lokalne lub wymagały uwierzytelnienia przed pobraniem treści każdego użytkownika. Transmitery ogólne Amethyst używa tych przekaźników, aby pobrać dla Ciebie posty. + Podłączone Transmitery + Aktualna lista używanych transmiterów Polecane Transmitery Dodaj te transmitery do głównej listy, aby otrzymywać wiadomości od wymienionych użytkowników. Transmitery wyszukujące Lista transmiterów używanych podczas wyszukiwania treści lub użytkowników. Tagowanie i wyszukiwanie nie będą działać, jeśli nie będą dostępne żadne opcje. Upewnij się, że zaimplementowano NIP-50. Lokalne Transmitery Lista transmiterów działających na tym urządzeniu. + Zaufane Transmitery + Zaufane Transmitery + Transmitery, którym ufasz, nie potrzebujesz połączenia Tor + Transmitery Proxy + Transmitery Proxy + Transmitery agregatora, z których aplikacja musi pobierać kanały, takie jak filter.nostr.wine. To zastępuje model skrzynki nadawczej i sprawia, że aplikacja łączy się tylko z transmiterami z listy. + Transmitery Nadawcze + Transmitery Nadawania + Transmitery, które specjalizują się w przesyłaniu wpisów do wszystkich innych transmiterów, takich jak sendit.nosflare.com. Amethyst doda ten transmiter do wszystkich nowych wydarzeń utworzonych przez użytkownika + Transmitery Indeksera + Transmitery indeksera + Transmitery, które specjalizują się w hostowaniu metadanych i list wszystkich użytkowników, takie jak purplepag.es. Amethyst użyje tych transmiterów do znalezienia użytkowników, którzy nie znajdują się na twoich listach. + Zablokowane Transmitery + Zablokowane Transmitery + Amethyst nigdy nie połączy się z tymi transmiterami Wspieraj deweloperów! Twoja darowizna pomaga nam coś zmienić. Każdy sat się liczy! Przekaż darowiznę @@ -968,6 +1002,7 @@ Wybierz listę, aby filtrować kanał Wyloguj się przy blokowaniu urządzenia Wiadomość prywatna + Publiczna wiadomość Transmiter Czatu Transmiter, z którym łączą się wszyscy użytkownicy tego czatu Udostępnij zdjęcie…