Moves Coil's image loader choice to a subscription on account.

This commit is contained in:
Vitor Pamplona
2025-04-29 16:04:16 -04:00
parent 7c68ffaf6e
commit 9df0f6c368
11 changed files with 151 additions and 41 deletions
@@ -163,10 +163,11 @@ class Amethyst : Application() {
fun contentResolverFn(): ContentResolver = contentResolver fun contentResolverFn(): ContentResolver = contentResolver
fun setImageLoader(shouldUseTor: Boolean?) = fun setImageLoader(shouldUseTor: (String) -> Boolean?) {
ImageLoaderSetup.setup(this, diskCache, memoryCache) { ImageLoaderSetup.setup(this, diskCache, memoryCache) { url ->
shouldUseTor?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false) shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
} }
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub) fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
@@ -61,8 +61,6 @@ class ServiceManager(
val myAccount = account val myAccount = account
Amethyst.instance.setImageLoader(myAccount?.shouldUseTorForImageDownload())
if (myAccount != null) { if (myAccount != null) {
val relaySet = myAccount.connectToRelaysWithProxy.value val relaySet = myAccount.connectToRelaysWithProxy.value
client.reconnect(relaySet) client.reconnect(relaySet)
@@ -618,6 +618,7 @@ class Account(
listName, listName,
location = Amethyst.instance.locationManager.geohashStateFlow, location = Amethyst.instance.locationManager.geohashStateFlow,
) )
else -> { else -> {
val note = LocalCache.checkGetOrCreateAddressableNote(listName) val note = LocalCache.checkGetOrCreateAddressableNote(listName)
if (note != null) { if (note != null) {
@@ -1126,7 +1127,7 @@ class Account(
class EmojiMedia( class EmojiMedia(
val code: String, val code: String,
val url: MediaUrlImage, val link: MediaUrlImage,
) )
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent
@@ -1173,7 +1174,7 @@ class Account(
null null
} }
}.flatten() }.flatten()
.distinctBy { it.url } .distinctBy { it.link }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val myEmojis by lazy { val myEmojis by lazy {
@@ -3646,11 +3647,33 @@ class Account(
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME)
fun shouldUseTorForImageDownload() = fun shouldUseTorForImageDownload(url: String) =
when (settings.torSettings.torType.value) { shouldUseTorFor(
TorType.OFF -> false url,
TorType.INTERNAL -> settings.torSettings.imagesViaTor.value settings.torSettings.torType.value,
TorType.EXTERNAL -> settings.torSettings.imagesViaTor.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() = fun shouldUseTorForVideoDownload() =
@@ -24,12 +24,19 @@ import android.app.Application
import android.os.Build import android.os.Build
import coil3.ImageLoader import coil3.ImageLoader
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.Uri
import coil3.annotation.DelicateCoilApi import coil3.annotation.DelicateCoilApi
import coil3.annotation.ExperimentalCoilApi
import coil3.disk.DiskCache import coil3.disk.DiskCache
import coil3.fetch.Fetcher
import coil3.gif.AnimatedImageDecoder import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder import coil3.gif.GifDecoder
import coil3.memory.MemoryCache 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.size.Precision
import coil3.svg.SvgDecoder import coil3.svg.SvgDecoder
import coil3.util.DebugLogger import coil3.util.DebugLogger
@@ -38,12 +45,22 @@ import okhttp3.Call
class ImageLoaderSetup { class ImageLoaderSetup {
companion object { 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) @OptIn(DelicateCoilApi::class)
fun setup( fun setup(
app: Application, app: Application,
diskCache: DiskCache, diskCache: DiskCache,
memoryCache: MemoryCache, memoryCache: MemoryCache,
callFactory: () -> Call.Factory, callFactory: (url: String) -> Call.Factory,
) { ) {
SingletonImageLoader.setUnsafe( SingletonImageLoader.setUnsafe(
ImageLoader ImageLoader
@@ -51,21 +68,81 @@ class ImageLoaderSetup {
.diskCache { diskCache } .diskCache { diskCache }
.memoryCache { memoryCache } .memoryCache { memoryCache }
.precision(Precision.INEXACT) .precision(Precision.INEXACT)
.logger(if (isDebug) DebugLogger() else null) .logger(debugLogger)
.components { .components {
if (Build.VERSION.SDK_INT >= 28) { add(gifFactory)
add(AnimatedImageDecoder.Factory()) add(svgFactory)
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
add(Base64Fetcher.Factory) add(Base64Fetcher.Factory)
add(BlurHashFetcher.Factory) add(BlurHashFetcher.Factory)
add(Base64Fetcher.BKeyer) add(Base64Fetcher.BKeyer)
add(BlurHashFetcher.BKeyer) add(BlurHashFetcher.BKeyer)
add(OkHttpNetworkFetcherFactory(callFactory)) add(OkHttpFactory(callFactory))
}.build(), }.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<Uri> {
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 <P, T> singleParameterLazy(initializer: (P) -> T) = SingleParameterLazy(initializer)
internal class SingleParameterLazy<P, T>(
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
@@ -879,7 +879,7 @@ open class NewPostViewModel :
): List<EmojiUrlTag> { ): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList() if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> 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) { open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
val wordToInsert = item.url.url + " " val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare( iMetaAttachments.downloadAndPrepare(
item.url.url, item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
) )
} }
@@ -80,7 +80,7 @@ fun ShowEmojiSuggestionList(
horizontalArrangement = spacedBy(Size10dp), horizontalArrangement = spacedBy(Size10dp),
) { ) {
Box(Size40Modifier) { Box(Size40Modifier) {
UrlImageView(it.url, accountViewModel) UrlImageView(it.link, accountViewModel)
} }
Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Box(Size40Modifier, contentAlignment = Alignment.Center) { Box(Size40Modifier, contentAlignment = Alignment.Center) {
@@ -1522,7 +1522,7 @@ class AccountViewModel(
fun okHttpClientForNip96(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url)) 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)) fun okHttpClientForVideo(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url))
@@ -84,9 +84,13 @@ fun LoggedInPage(
// Adds this account to the authentication procedures for relays. // Adds this account to the authentication procedures for relays.
RelayAuthSubscription(accountViewModel) 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) AccountFilterAssemblerSubscription(accountViewModel)
// Pre-loads each of the main screens.
HomeFilterAssemblerSubscription(accountViewModel) HomeFilterAssemblerSubscription(accountViewModel)
ChatroomListFilterAssemblerSubscription(accountViewModel) ChatroomListFilterAssemblerSubscription(accountViewModel)
VideoFilterAssemblerSubscription(accountViewModel) VideoFilterAssemblerSubscription(accountViewModel)
@@ -124,6 +128,13 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) {
Amethyst.instance.cache.antiSpam.active = isSpamActive Amethyst.instance.cache.antiSpam.active = isSpamActive
} }
@Composable
fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) {
LaunchedEffect(Unit) {
Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload)
}
}
@Composable @Composable
fun ManageRelayServices( fun ManageRelayServices(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -502,7 +502,7 @@ class ChatNewMessageViewModel :
): List<EmojiUrlTag> { ): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList() if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> 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) { fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
val wordToInsert = item.url.url + " " val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare( iMetaAttachments.downloadAndPrepare(
item.url.url, item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
) )
} }
@@ -464,7 +464,7 @@ open class ChannelNewMessageViewModel :
): List<EmojiUrlTag> { ): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList() if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> 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) { open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
val wordToInsert = item.url.url + " " val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare( iMetaAttachments.downloadAndPrepare(
item.url.url, item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
) )
} }
@@ -374,7 +374,7 @@ open class NewProductViewModel :
): List<EmojiUrlTag> { ): List<EmojiUrlTag> {
if (myEmojiSet == null) return emptyList() if (myEmojiSet == null) return emptyList()
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> 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) { open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
val wordToInsert = item.url.url + " " val wordToInsert = item.link.url + " "
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
iMetaDescription.downloadAndPrepare( iMetaDescription.downloadAndPrepare(
item.url.url, item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
) )
} }