diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 82ef4e015..966c8d39e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -163,10 +163,11 @@ class Amethyst : Application() { fun contentResolverFn(): ContentResolver = contentResolver - fun setImageLoader(shouldUseTor: Boolean?) = - ImageLoaderSetup.setup(this, diskCache, memoryCache) { - shouldUseTor?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false) + fun setImageLoader(shouldUseTor: (String) -> Boolean?) { + ImageLoaderSetup.setup(this, diskCache, memoryCache) { url -> + shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false) } + } fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 12a591746..838c22f57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -61,8 +61,6 @@ class ServiceManager( val myAccount = account - Amethyst.instance.setImageLoader(myAccount?.shouldUseTorForImageDownload()) - if (myAccount != null) { val relaySet = myAccount.connectToRelaysWithProxy.value client.reconnect(relaySet) 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 667e70a79..342b9d124 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -618,6 +618,7 @@ class Account( listName, location = Amethyst.instance.locationManager.geohashStateFlow, ) + else -> { val note = LocalCache.checkGetOrCreateAddressableNote(listName) if (note != null) { @@ -1126,7 +1127,7 @@ class Account( class EmojiMedia( val code: String, - val url: MediaUrlImage, + val link: MediaUrlImage, ) fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent @@ -1173,7 +1174,7 @@ class Account( null } }.flatten() - .distinctBy { it.url } + .distinctBy { it.link } @OptIn(ExperimentalCoroutinesApi::class) val myEmojis by lazy { @@ -3646,11 +3647,33 @@ class Account( fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) - fun shouldUseTorForImageDownload() = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.imagesViaTor.value - TorType.EXTERNAL -> settings.torSettings.imagesViaTor.value + 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 (isLocalHost(normalizedUrl)) { + false + } else if (isOnionUrl(normalizedUrl)) { + true + } else { + final } fun shouldUseTorForVideoDownload() = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt index 72806c7aa..96cd50006 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt @@ -24,12 +24,19 @@ import android.app.Application import android.os.Build import coil3.ImageLoader import coil3.SingletonImageLoader +import coil3.Uri import coil3.annotation.DelicateCoilApi +import coil3.annotation.ExperimentalCoilApi import coil3.disk.DiskCache +import coil3.fetch.Fetcher import coil3.gif.AnimatedImageDecoder import coil3.gif.GifDecoder import coil3.memory.MemoryCache -import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.network.CacheStrategy +import coil3.network.ConnectivityChecker +import coil3.network.NetworkFetcher +import coil3.network.okhttp.asNetworkClient +import coil3.request.Options import coil3.size.Precision import coil3.svg.SvgDecoder import coil3.util.DebugLogger @@ -38,12 +45,22 @@ import okhttp3.Call class ImageLoaderSetup { companion object { + val gifFactory = + if (Build.VERSION.SDK_INT >= 28) { + AnimatedImageDecoder.Factory() + } else { + GifDecoder.Factory() + } + val svgFactory = SvgDecoder.Factory() + + val debugLogger = if (isDebug) DebugLogger() else null + @OptIn(DelicateCoilApi::class) fun setup( app: Application, diskCache: DiskCache, memoryCache: MemoryCache, - callFactory: () -> Call.Factory, + callFactory: (url: String) -> Call.Factory, ) { SingletonImageLoader.setUnsafe( ImageLoader @@ -51,21 +68,81 @@ class ImageLoaderSetup { .diskCache { diskCache } .memoryCache { memoryCache } .precision(Precision.INEXACT) - .logger(if (isDebug) DebugLogger() else null) + .logger(debugLogger) .components { - if (Build.VERSION.SDK_INT >= 28) { - add(AnimatedImageDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - add(SvgDecoder.Factory()) + add(gifFactory) + add(svgFactory) add(Base64Fetcher.Factory) add(BlurHashFetcher.Factory) add(Base64Fetcher.BKeyer) add(BlurHashFetcher.BKeyer) - add(OkHttpNetworkFetcherFactory(callFactory)) + add(OkHttpFactory(callFactory)) }.build(), ) } } } + +/** + * Copied from Coil to allow networkClient to be a function of the url. + * So that Tor and non Tor clients can be used. + */ +@OptIn(ExperimentalCoilApi::class) +class OkHttpFactory( + val networkClient: (url: String) -> Call.Factory, +) : Fetcher.Factory { + private val cacheStrategyLazy = lazy { CacheStrategy.DEFAULT } + private val connectivityCheckerLazy = singleParameterLazy(::ConnectivityChecker) + + override fun create( + data: Uri, + options: Options, + imageLoader: ImageLoader, + ): Fetcher? { + if (!isApplicable(data)) return null + + val url = data.toString() + + return NetworkFetcher( + url = url, + options = options, + networkClient = lazy { networkClient(url).asNetworkClient() }, + diskCache = lazy { imageLoader.diskCache }, + cacheStrategy = cacheStrategyLazy, + connectivityChecker = connectivityCheckerLazy.get(options.context), + ) + } + + private fun isApplicable(data: Uri): Boolean = data.scheme == "http" || data.scheme == "https" +} + +internal fun singleParameterLazy(initializer: (P) -> T) = SingleParameterLazy(initializer) + +internal class SingleParameterLazy( + initializer: (P) -> T, +) : Any() { + private var initializer: ((P) -> T)? = initializer + private var value: Any? = UNINITIALIZED + + @Suppress("UNCHECKED_CAST") + fun get(parameter: P): T { + val value1 = value + if (value1 !== UNINITIALIZED) { + return value1 as T + } + + return synchronized(this) { + val value2 = value + if (value2 !== UNINITIALIZED) { + value2 as T + } else { + val newValue = initializer!!(parameter) + value = newValue + initializer = null + newValue + } + } + } +} + +private object UNINITIALIZED diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index cf6bc4664..54622d53d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -879,7 +879,7 @@ open class NewPostViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } } } @@ -1078,12 +1078,12 @@ open class NewPostViewModel : } open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) { - val wordToInsert = item.url.url + " " + val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( - item.url.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, + item.link.url, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt index b4275c89b..c08fcf39d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt @@ -80,7 +80,7 @@ fun ShowEmojiSuggestionList( horizontalArrangement = spacedBy(Size10dp), ) { Box(Size40Modifier) { - UrlImageView(it.url, accountViewModel) + UrlImageView(it.link, accountViewModel) } Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) Box(Size40Modifier, contentAlignment = Alignment.Center) { 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 bf9b24944..d5e251a02 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 @@ -1522,7 +1522,7 @@ class AccountViewModel( fun okHttpClientForNip96(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url)) - fun okHttpClientForImage(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload()) + fun okHttpClientForImage(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url)) fun okHttpClientForVideo(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url)) 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 3db70dac4..4ba0f36e4 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 @@ -84,9 +84,13 @@ fun LoggedInPage( // Adds this account to the authentication procedures for relays. RelayAuthSubscription(accountViewModel) - // Loads account information from Relays. + // Sets up Coil's Image Loader + ObserveImageLoadingTor(accountViewModel) + + // Loads account information + DMs and Notifications from Relays. AccountFilterAssemblerSubscription(accountViewModel) + // Pre-loads each of the main screens. HomeFilterAssemblerSubscription(accountViewModel) ChatroomListFilterAssemblerSubscription(accountViewModel) VideoFilterAssemblerSubscription(accountViewModel) @@ -124,6 +128,13 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) { Amethyst.instance.cache.antiSpam.active = isSpamActive } +@Composable +fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { + LaunchedEffect(Unit) { + Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload) + } +} + @Composable fun ManageRelayServices( accountViewModel: AccountViewModel, 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 31cf1db37..8054a59e9 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 @@ -502,7 +502,7 @@ class ChatNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } } } @@ -647,12 +647,12 @@ class ChatNewMessageViewModel : } fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) { - val wordToInsert = item.url.url + " " + val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( - item.url.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, + item.link.url, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } 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 6bb433c19..57f0c62b2 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 @@ -464,7 +464,7 @@ open class ChannelNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } } } @@ -561,12 +561,12 @@ open class ChannelNewMessageViewModel : } open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) { - val wordToInsert = item.url.url + " " + val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( - item.url.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, + item.link.url, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/marketplace/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/marketplace/NewProductViewModel.kt index a537379de..5db97583f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/marketplace/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/marketplace/NewProductViewModel.kt @@ -374,7 +374,7 @@ open class NewProductViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.url.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } } } @@ -555,12 +555,12 @@ open class NewProductViewModel : } open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) { - val wordToInsert = item.url.url + " " + val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { iMetaDescription.downloadAndPrepare( - item.url.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, + item.link.url, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) }