From 8e6d71e4223235846394df7476c6fce98b51fe6f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Jul 2025 19:09:06 -0400 Subject: [PATCH] Minimizes the dependency on the Account further by moving Tor settings to a tor Privacy State --- .../vitorpamplona/amethyst/model/Account.kt | 213 +++--------------- .../amethyst/model/LocalCache.kt | 4 +- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 17 +- .../model/privacyOptions/PrivacyState.kt | 125 ++++++++++ .../model/privateChats/ChatroomList.kt | 4 - .../amethyst/model/torState/TorRelayState.kt | 2 + ...Builder.kt => OkHttpOtsResolverBuilder.kt} | 31 +-- .../service/uploads/UploadOrchestrator.kt | 8 +- .../ui/actions/NewUserMetadataScreen.kt | 5 +- .../ui/actions/NewUserMetadataViewModel.kt | 54 +++-- .../ui/navigation/routes/RouteMaker.kt | 6 +- .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 83 ++----- .../loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../ui/screen/loggedIn/LoggedInPage.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 4 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 2 +- .../loggedIn/profile/header/MessageButton.kt | 12 +- .../relays/common/BasicRelaySetupInfoModel.kt | 2 +- .../relays/nip65/Nip65RelayListViewModel.kt | 4 +- .../quartz/nip57Zaps/PrivateZapTests.kt | 4 +- .../nip03Timestamp/OtsResolverBuilder.kt | 25 ++ .../nip03Timestamp/VerificationStateCache.kt | 8 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 14 +- 29 files changed, 305 insertions(+), 342 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/{OtsResolverBuilder.kt => OkHttpOtsResolverBuilder.kt} (69%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 415463f73..8f233b3b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState +import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState import com.vitorpamplona.amethyst.model.serverList.MergedServerListState @@ -79,9 +80,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder +import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder import com.vitorpamplona.amethyst.service.uploads.FileHeader -import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent @@ -118,7 +118,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirst import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses @@ -130,7 +129,6 @@ 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.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.reply @@ -302,14 +300,17 @@ class Account( val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) - val otsResolver: OtsResolver = - OtsResolverBuilder().build( + val privacyState = PrivacyState(settings) + val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) + + val otsResolverBuilder: OkHttpOtsResolverBuilder = + OkHttpOtsResolverBuilder( Amethyst.instance.okHttpClients, - ::shouldUseTorForMoneyOperations, + privacyState::shouldUseTorForMoneyOperations, Amethyst.instance.otsBlockHeightCache, ) - val otsState = OtsState(signer, cache, otsResolver, scope, settings) + val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings) val feedDecryptionCaches = FeedDecryptionCaches( @@ -389,8 +390,6 @@ class Account( */ - val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) - fun isWriteable(): Boolean = settings.isWriteable() suspend fun updateWarnReports(warnReports: Boolean): Boolean { @@ -463,67 +462,15 @@ class Account( private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData()) - suspend fun sendNewUserMetadata( - name: String? = null, - picture: String? = null, - banner: String? = null, - website: String? = null, - pronouns: String? = null, - about: String? = null, - nip05: String? = null, - lnAddress: String? = null, - lnURL: String? = null, - twitter: String? = null, - mastodon: String? = null, - github: String? = null, - ) { - if (!isWriteable()) return - - val userMetadataEvent = - userMetadata.sendNewUserMetadata( - name, - picture, - banner, - website, - pronouns, - about, - nip05, - lnAddress, - lnURL, - twitter, - mastodon, - github, - ) - - sendLiterallyEverywhere(userMetadataEvent) - } - - fun reactionTo( - note: Note, - reaction: String, - ): List = note.reactedBy(userProfile(), reaction) - - fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty() - - fun boostsTo(note: Note): List = note.boostedBy(userProfile()) - - fun hasReacted( - note: Note, - reaction: String, - ): Boolean = note.hasReacted(userProfile(), reaction) - suspend fun reactTo( note: Note, reaction: String, ) = ReactionAction.reactTo( - note, - reaction, - userProfile(), - signer, - onPublic = { - client.send(it, computeMyReactionToNote(note, it)) - cache.justConsumeMyOwnEvent(it) - }, + note = note, + reaction = reaction, + by = userProfile(), + signer = signer, + onPublic = ::sendAutomatic, onPrivate = ::broadcastPrivately, ) @@ -534,18 +481,15 @@ class Account( zapType: LnZapEvent.ZapType, toUser: User?, additionalRelays: Set? = null, - ): LnZapRequestEvent { - val relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()) - return LnZapRequestEvent.create( - event, - relays = relays.mapTo(mutableSetOf()) { it.url }, - signer, - pollOption, - message, - zapType, - toUser?.pubkeyHex, - ) - } + ) = LnZapRequestEvent.create( + zappedEvent = event, + relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()), + signer = signer, + pollOption = pollOption, + message = message, + zapType = zapType, + toUserPubHex = toUser?.pubkeyHex, + ) suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note?, @@ -570,12 +514,10 @@ class Account( message: String = "", zapType: LnZapEvent.ZapType, ): LnZapRequestEvent { - val relays = nip65RelayList.inboxFlow.value + user.inboxRelays() - val zapRequest = LnZapRequestEvent.create( userHex = user.pubkeyHex, - relays = relays, + relays = nip65RelayList.inboxFlow.value + user.inboxRelays(), signer = signer, message = message, zapType = zapType, @@ -596,9 +538,7 @@ class Account( type: ReportType, ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer)) - suspend fun delete(note: Note) { - delete(listOf(note)) - } + suspend fun delete(note: Note) = delete(listOf(note)) suspend fun delete(notes: List) { if (!isWriteable()) return @@ -627,10 +567,7 @@ class Account( if (!isWriteable()) return if (event.pubKey != signer.pubKey) return - val deletionEvent = - signer.sign( - DeletionEvent.build(listOf(event)), - ) + val deletionEvent = signer.sign(DeletionEvent.build(listOf(event))) client.send(deletionEvent, outboxRelays.flow.value + additionalRelays) cache.justConsumeMyOwnEvent(deletionEvent) } @@ -1822,104 +1759,6 @@ class Account( fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) - fun shouldUseTorForImageDownload(url: String) = - shouldUseTorFor( - url, - settings.torSettings.torType.value, - settings.torSettings.imagesViaTor.value, - ) - - fun shouldUseTorFor( - url: String, - torType: TorType, - imagesViaTor: Boolean, - ) = when (torType) { - TorType.OFF -> false - TorType.INTERNAL -> shouldUseTor(url, imagesViaTor) - TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor) - } - - private fun shouldUseTor( - normalizedUrl: String, - final: Boolean, - ): Boolean = - if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { - false - } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { - true - } else { - final - } - - fun shouldUseTorForVideoDownload() = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.videosViaTor.value - TorType.EXTERNAL -> settings.torSettings.videosViaTor.value - } - - fun shouldUseTorForVideoDownload(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) - } - - fun shouldUseTorForPreviewUrl(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) - } - - fun shouldUseTorForTrustedRelays() = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value - TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value - } - - fun shouldUseTorForClean(relay: NormalizedRelayUrl) = torRelayState.flow.value.useTor(relay) - - private fun checkLocalHostOnionAndThen( - url: String, - final: Boolean, - ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) - - private fun checkLocalHostOnionAndThen( - normalizedUrl: String, - isOnionRelaysActive: Boolean, - final: Boolean, - ): Boolean = - if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { - false - } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { - isOnionRelaysActive - } else { - final - } - - fun shouldUseTorForMoneyOperations(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) - } - - fun shouldUseTorForNIP05(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) - } - - fun shouldUseTorForNIP96(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) - } - init { Log.d("AccountRegisterObservers", "Init") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 2bf690c51..36bfec428 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -76,7 +76,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.VerificationState import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -2102,7 +2102,7 @@ object LocalCache : ILocalCache { suspend fun findEarliestOtsForNote( note: Note, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): Long? { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 5f5d3fbd9..204d98c5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -745,14 +745,14 @@ open class Note( fun hasReacted( loggedIn: User, content: String, - ): Boolean = reactedBy(loggedIn, content).isNotEmpty() + ): Boolean = allReactionsOfContentByAuthor(loggedIn, content).isNotEmpty() - fun reactedBy( + fun allReactionsOfContentByAuthor( loggedIn: User, content: String, ): List = reactions[content]?.filter { it.author == loggedIn } ?: emptyList() - fun reactedBy(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } + fun allReactionsByAuthor(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { val fiveMinsAgo = TimeUtils.fiveMinutesAgo() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index 961ff0b7f..44f316caa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -24,10 +24,10 @@ import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.update import java.util.Base64 @@ -35,7 +35,7 @@ import java.util.Base64 class OtsState( val signer: NostrSigner, val cache: LocalCache, - val otsResolver: OtsResolver, + val otsResolver: OkHttpOtsResolverBuilder, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -43,7 +43,7 @@ class OtsState( Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") return settings.pendingAttestations.value.toList().mapNotNull { (key, value) -> - val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver) + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build()) if (otsState != null) { val hint = cache.getNoteIfExists(key)?.toEventHint() @@ -75,6 +75,15 @@ class OtsState( val id = note.event?.id ?: note.idHex - settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) + settings.addPendingAttestation( + id = id, + stamp = + Base64.getEncoder().encodeToString( + OtsEvent.stamp( + id, + otsResolver.build(), + ), + ), + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt new file mode 100644 index 000000000..dcbee7e43 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt @@ -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.amethyst.model.privacyOptions + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.ui.tor.TorType +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer + +class PrivacyState( + val settings: AccountSettings, +) { + fun shouldUseTorForImageDownload(url: String) = + shouldUseTorFor( + url, + settings.torSettings.torType.value, + settings.torSettings.imagesViaTor.value, + ) + + fun shouldUseTorFor( + url: String, + torType: TorType, + imagesViaTor: Boolean, + ) = when (torType) { + TorType.OFF -> false + TorType.INTERNAL -> shouldUseTor(url, imagesViaTor) + TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor) + } + + private fun shouldUseTor( + normalizedUrl: String, + final: Boolean, + ): Boolean = + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { + false + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { + true + } else { + final + } + + fun shouldUseTorForVideoDownload() = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> settings.torSettings.videosViaTor.value + TorType.EXTERNAL -> settings.torSettings.videosViaTor.value + } + + fun shouldUseTorForVideoDownload(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) + } + + fun shouldUseTorForPreviewUrl(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) + } + + fun shouldUseTorForTrustedRelays() = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value + TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value + } + + private fun checkLocalHostOnionAndThen( + url: String, + final: Boolean, + ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) + + private fun checkLocalHostOnionAndThen( + normalizedUrl: String, + isOnionRelaysActive: Boolean, + final: Boolean, + ): Boolean = + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { + false + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { + isOnionRelaysActive + } else { + final + } + + fun shouldUseTorForMoneyOperations(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) + } + + fun shouldUseTorForNIP05(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) + } + + fun shouldUseTorForUploads(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index 797070e57..f73f4790e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -65,10 +65,6 @@ class ChatroomList( } } - fun createChatroom(withKey: ChatroomKey) { - getOrCreatePrivateChatroom(withKey) - } - fun removeMessage( user: User, msg: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index db68542b4..f1190941e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -115,4 +115,6 @@ class TorRelayState( dmRelayList = dmRelayState.flow.value, ), ) + + fun shouldUseTorForClean(relay: NormalizedRelayUrl) = flow.value.useTor(relay) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt similarity index 69% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt index 5925ea657..d858cdd6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt @@ -22,8 +22,13 @@ package com.vitorpamplona.amethyst.service.ots import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder -class OtsResolverBuilder { +class OkHttpOtsResolverBuilder( + val okHttpClients: DualHttpClientManager, + val shouldUseTorForUrl: (String) -> Boolean, + val cache: OtsBlockHeightCache, +) : OtsResolverBuilder { fun getAPI(usingTor: Boolean) = if (usingTor) { OkHttpBitcoinExplorer.MEMPOOL_API_URL @@ -31,22 +36,20 @@ class OtsResolverBuilder { OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL } - fun build( - okHttpClients: DualHttpClientManager, - shouldUseTorForUrl: (String) -> Boolean, - cache: OtsBlockHeightCache, - ): OtsResolver { + override fun build(): OtsResolver { val shouldUseTor = shouldUseTorForUrl(OkHttpBitcoinExplorer.MEMPOOL_API_URL) return OtsResolver( - OkHttpBitcoinExplorer( - getAPI(shouldUseTor), - okHttpClients.getHttpClient(shouldUseTor), - cache, - ), - OkHttpCalendarBuilder { - okHttpClients.getHttpClient(shouldUseTorForUrl(it)) - }, + explorer = + OkHttpBitcoinExplorer( + baseAPI = getAPI(usingTor = shouldUseTor), + client = okHttpClients.getHttpClient(shouldUseTor), + cache = cache, + ), + calendarBuilder = + OkHttpCalendarBuilder { + okHttpClients.getHttpClient(shouldUseTorForUrl(it)) + }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index c6859a460..4423bd16c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -152,7 +152,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = { percent: Float -> updateState(0.2 + (0.2 * percent), UploadingState.Uploading) }, @@ -165,7 +165,7 @@ class UploadOrchestrator { localContentType = contentType, originalContentType = contentTypeForResult, originalHash = originalHash, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, ) } catch (_: SignerExceptions.ReadOnlyException) { error(R.string.login_with_a_private_key_to_be_able_to_upload) @@ -198,7 +198,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) @@ -206,7 +206,7 @@ class UploadOrchestrator { verifyHeader( uploadResult = result, localContentType = contentType, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, originalHash = originalHash, originalContentType = contentTypeForResult, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index ddb143880..6d8b8c1f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -58,10 +58,11 @@ fun NewUserMetadataScreen( accountViewModel: AccountViewModel, ) { val postViewModel: NewUserMetadataViewModel = viewModel() + postViewModel.init(accountViewModel) val context = LocalContext.current - LaunchedEffect(Unit) { - postViewModel.load(accountViewModel.account) + LaunchedEffect(accountViewModel) { + postViewModel.load() } Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index c24c121cd..5d0bc3222 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -35,17 +34,17 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException class NewUserMetadataViewModel : ViewModel() { + private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account // val userName = mutableStateOf("") @@ -68,9 +67,12 @@ class NewUserMetadataViewModel : ViewModel() { var isUploadingImageForPicture by mutableStateOf(false) var isUploadingImageForBanner by mutableStateOf(false) - fun load(account: Account) { - this.account = account + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + } + fun load() { account.userProfile().let { // userName.value = it.bestUsername() ?: "" displayName.value = it.info?.bestName() ?: "" @@ -100,21 +102,25 @@ class NewUserMetadataViewModel : ViewModel() { fun create() { // Tries to not delete any existing attribute that we do not work with. - viewModelScope.launch(Dispatchers.IO) { - account.sendNewUserMetadata( - name = displayName.value, - picture = picture.value, - banner = banner.value, - website = website.value, - pronouns = pronouns.value, - about = about.value, - nip05 = nip05.value, - lnAddress = lnAddress.value, - lnURL = lnURL.value, - twitter = twitter.value, - mastodon = mastodon.value, - github = github.value, - ) + accountViewModel.runIOCatching { + val metadata = + account.userMetadata.sendNewUserMetadata( + name = displayName.value, + picture = picture.value, + banner = banner.value, + website = website.value, + pronouns = pronouns.value, + about = about.value, + nip05 = nip05.value, + lnAddress = lnAddress.value, + lnURL = lnURL.value, + twitter = twitter.value, + mastodon = mastodon.value, + github = github.value, + ) + + account.sendLiterallyEverywhere(metadata) + clear() } } @@ -139,7 +145,7 @@ class NewUserMetadataViewModel : ViewModel() { context: Context, onError: (String, String) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { upload( uri, context, @@ -155,7 +161,7 @@ class NewUserMetadataViewModel : ViewModel() { context: Context, onError: (String, String) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { upload( uri, context, @@ -187,7 +193,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -200,7 +206,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index dc1260c58..cd5fbcf93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -79,7 +79,7 @@ fun routeFor( } } else if (innerEvent is ChatroomKeyable) { val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) - loggedIn.chatroomList.createChatroom(room) + loggedIn.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room) } else if (innerEvent is AddressableEvent) { return Route.Note(noteEvent.aTag().toTag()) @@ -104,7 +104,7 @@ fun routeFor( } } else if (noteEvent is ChatroomKeyable) { val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) - loggedIn.chatroomList.createChatroom(room) + loggedIn.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room) } else if (noteEvent is CommunityDefinitionEvent) { return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) @@ -169,7 +169,7 @@ fun routeToMessage( draftId: HexKey? = null, account: Account, ): Route { - account.chatroomList.createChatroom(room) + account.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room, draftMessage, replyId, draftId) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 303b2ea96..b40b7357b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -1355,7 +1355,7 @@ fun ReactionChoicePopup( val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle() - val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() } + val toRemove = remember { baseNote.allReactionsByAuthor(accountViewModel.userProfile()).toImmutableSet() } Popup( alignment = Alignment.BottomCenter, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 3e763cb79..61cf44742 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -623,7 +623,7 @@ open class CommentPostViewModel : iMetaAttachments.downloadAndPrepare(item.link.url) { Amethyst.Companion.instance.okHttpClients .getHttpClient( - accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false, ) } 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 bbf0cee47..be94cc1c7 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 @@ -98,7 +98,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -266,13 +265,6 @@ class AccountViewModel( fun userProfile(): User = account.userProfile() - suspend fun reactTo( - note: Note, - reaction: String, - ) { - account.reactTo(note, reaction) - } - fun observeByETag( kind: Int, eTag: HexKey, @@ -287,8 +279,8 @@ class AccountViewModel( note: Note, reaction: String, ) { - viewModelScope.launch(Dispatchers.IO) { - val currentReactions = account.reactionTo(note, reaction) + runIOCatching { + val currentReactions = note.allReactionsOfContentByAuthor(userProfile(), reaction) if (currentReactions.isNotEmpty()) { account.delete(currentReactions) } else { @@ -298,14 +290,8 @@ class AccountViewModel( } fun reactToOrDelete(note: Note) { - viewModelScope.launch(Dispatchers.IO) { - val reaction = reactionChoices().first() - if (hasReactedTo(note, reaction)) { - deleteReactionTo(note, reaction) - } else { - reactTo(note, reaction) - } - } + val reaction = reactionChoices().first() + reactToOrDelete(note, reaction) } @Immutable @@ -402,24 +388,6 @@ class AccountViewModel( noteMustShowExpandButtonFlows.put(note, it) } - fun hasReactedTo( - baseNote: Note, - reaction: String, - ): Boolean = account.hasReacted(baseNote, reaction) - - suspend fun deleteReactionTo( - note: Note, - reaction: String, - ) { - account.delete(account.reactionTo(note, reaction)) - } - - fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote) - - fun deleteBoostsTo(note: Note) { - runIOCatching { account.delete(account.boostsTo(note)) } - } - suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note, onWasZapped: (Boolean) -> Unit, @@ -977,7 +945,7 @@ class AccountViewModel( .verifyNip05( nip05, okHttpClient = { - app.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it)) + app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForNIP05(it)) }, onSuccess = { // Marks user as verified @@ -1103,7 +1071,7 @@ class AccountViewModel( ) { onResult( withContext(Dispatchers.Default) { - LocalCache.findEarliestOtsForNote(note, account::otsResolver) + LocalCache.findEarliestOtsForNote(note, account.otsResolverBuilder) }, ) } @@ -1228,21 +1196,7 @@ class AccountViewModel( } } - fun createChatRoomFor( - user: User, - then: (ChatroomKey) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) - account.chatroomList.createChatroom(withKey) - then(withKey) - } - } - - fun setTorSettings(newTorSettings: TorSettings) = - viewModelScope.launch(Dispatchers.IO) { - account.settings.setTorSettings(newTorSettings) - } + fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) } class Factory( val accountSettings: AccountSettings, @@ -1357,8 +1311,11 @@ class AccountViewModel( } if (isWriteable()) { - if (hasBoosted(baseNote)) { - deleteBoostsTo(baseNote) + val boosts = baseNote.boostedBy(userProfile()) + if (boosts.isNotEmpty()) { + runIOCatching { + account.delete(boosts) + } } else { onMore() } @@ -1496,21 +1453,21 @@ class AccountViewModel( } } - fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url)) + fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url)) - fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url)) + fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(url)) - fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url)) + fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url)) - fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url)) + fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url)) - fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url)) + fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url)) - fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url)) + fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url)) - fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForClean(url)) + fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(url)) - fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays()) + fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays()) fun dataSources() = app.sources 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 2343b07b7..13f513dd2 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 @@ -85,7 +85,7 @@ class DecryptAndIndexProcessor( is OtsEvent -> { // verifies new OTS upon arrival - Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver) + Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) } is DraftEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 39312e94d..b5e831e9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -130,7 +130,7 @@ fun SetProxyDeterminator(accountViewModel: AccountViewModel) { @Composable fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { LaunchedEffect(accountViewModel) { - Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload) + Amethyst.instance.setImageLoader(accountViewModel.account.privacyState::shouldUseTorForImageDownload) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 2001c2969..f2503744d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -640,7 +640,7 @@ class ChatNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.shouldUseTorForImageDownload(item.link.url)) + Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 403dc47a4..299519557 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -210,7 +210,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -223,7 +223,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 0468e5434..5f04be6ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -588,7 +588,7 @@ open class ChannelNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index b1108a34c..33b35373a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -541,7 +541,7 @@ open class NewProductViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaDescription.downloadAndPrepare( item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 096b5ea76..49cb45da9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -786,7 +786,7 @@ open class ShortNotePostViewModel : item.link.url, ) { Amethyst.instance.okHttpClients.getHttpClient( - accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index f2a477963..16bf12de6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -36,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey @Composable fun MessageButton( @@ -49,12 +51,10 @@ fun MessageButton( .padding(horizontal = 3.dp) .width(50.dp), onClick = { - accountViewModel.createChatRoomFor(user) { chatroomKey -> - nav.nav( - routeToMessage( - room = chatroomKey, - account = accountViewModel.account, - ), + nav.nav { + routeToMessage( + room = ChatroomKey(setOf(user.pubkeyHex)), + account = accountViewModel.account, ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index fabec89a7..9f84c96d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -72,7 +72,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { Nip11CachedRetriever.loadRelayInfo( relay = item.relay, okHttpClient = { - Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) + Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index a90f5aebe..c2d82c8c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -93,7 +93,7 @@ class Nip65RelayListViewModel : ViewModel() { _homeRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleHomePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -104,7 +104,7 @@ class Nip65RelayListViewModel : ViewModel() { _notificationRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index cd3056d96..70403da0e 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -92,7 +92,7 @@ class PrivateZapTests { val privateZapRequest = runBlocking { LnZapRequestEvent.create( - originalNote = poll, + zappedEvent = poll, relays = setOf("wss://relay.damus.io/"), signer = loggedIn, pollOption = 0, @@ -152,7 +152,7 @@ class PrivateZapTests { val privateZapRequest = runBlocking { LnZapRequestEvent.create( - originalNote = textNote, + zappedEvent = textNote, relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"), signer = loggedIn, pollOption = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt new file mode 100644 index 000000000..51c7f616d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt @@ -0,0 +1,25 @@ +/** + * 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.nip03Timestamp + +interface OtsResolverBuilder { + fun build(): OtsResolver +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt index 4b74a9a18..3e59d83bd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -29,15 +29,15 @@ class VerificationStateCache { fun verify( event: OtsEvent, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): VerificationState { cache.put(event.id, VerificationState.Verifying) - return event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } + return event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } } fun cacheVerify( event: OtsEvent, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): VerificationState = when (val verif = cache[event.id]) { is VerificationState.Verifying -> verif @@ -45,7 +45,7 @@ class VerificationStateCache { is VerificationState.NetworkError -> { // try again in 5 mins if (verif.time < TimeUtils.fiveMinutesAgo()) { - event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } + event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } } else { verify(event, resolverBuilder) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 48c937658..e2768c459 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -87,8 +87,8 @@ class LnZapRequestEvent( const val ALT = "Zap request" suspend fun create( - originalNote: Event, - relays: Set, + zappedEvent: Event, + relays: Set, signer: NostrSigner, pollOption: Int?, message: String, @@ -98,13 +98,13 @@ class LnZapRequestEvent( ): LnZapRequestEvent { var tags = listOf( - arrayOf("e", originalNote.id), - arrayOf("p", toUserPubHex ?: originalNote.pubKey), - arrayOf("relays") + relays, + arrayOf("e", zappedEvent.id), + arrayOf("p", toUserPubHex ?: zappedEvent.pubKey), + arrayOf("relays") + relays.map { it.url }, AltTag.assemble(ALT), ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(arrayOf("a", originalNote.aTag().toTag())) + if (zappedEvent is AddressableEvent) { + tags = tags + listOf(arrayOf("a", zappedEvent.aTag().toTag())) } if (pollOption != null && pollOption >= 0) { tags = tags + listOf(arrayOf(PollOptionTag.TAG_NAME, pollOption.toString()))