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 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)
@@ -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)
@@ -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() =
@@ -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<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> {
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) },
)
}
@@ -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) {
@@ -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))
@@ -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,
@@ -502,7 +502,7 @@ class ChatNewMessageViewModel :
): List<EmojiUrlTag> {
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) },
)
}
@@ -464,7 +464,7 @@ open class ChannelNewMessageViewModel :
): List<EmojiUrlTag> {
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) },
)
}
@@ -374,7 +374,7 @@ open class NewProductViewModel :
): List<EmojiUrlTag> {
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) },
)
}