Merge pull request #4347 from element-hq/feature/bma/coil3

Migrate to coil3
This commit is contained in:
Benoit Marty 2025-03-04 09:17:59 +01:00 committed by GitHub
commit e0de81c376
44 changed files with 172 additions and 127 deletions

View file

@ -5,12 +5,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(DelicateCoilApi::class)
package io.element.android.appnav package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import coil.Coil import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -69,7 +72,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
Coil.setImageLoader(imageLoaderHolder.get(inputs.matrixClient)) SingletonImageLoader.setUnsafe(imageLoaderHolder.get(inputs.matrixClient))
}, },
) )
} }

View file

@ -5,12 +5,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(DelicateCoilApi::class)
package io.element.android.appnav package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import coil.Coil import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@ -55,7 +58,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
Coil.setImageLoader(notLoggedInImageLoaderFactory) SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader())
}, },
) )
} }

View file

@ -9,7 +9,7 @@ package io.element.android.features.call.notifications
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import coil.ImageLoader import coil3.ImageLoader
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -22,9 +23,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImagePainter import coil3.Extras
import coil.compose.rememberAsyncImagePainter import coil3.compose.AsyncImagePainter
import coil.request.ImageRequest import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.location.api.internal.StaticMapPlaceholder import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.StaticMapUrlBuilder import io.element.android.features.location.api.internal.StaticMapUrlBuilder
@ -33,7 +35,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import timber.log.Timber
/** /**
* Shows a static map image downloaded via a third party service's static maps API. * Shows a static map image downloaded via a third party service's static maps API.
@ -75,14 +76,15 @@ fun StaticMapView(
) )
) )
.size(width = constraints.maxWidth, height = constraints.maxHeight) .size(width = constraints.maxWidth, height = constraints.maxHeight)
.setParameter("retry_hash", retryHash, memoryCacheKey = null) .apply {
extras.set(Extras.Key("retry_hash"), retryHash).build()
}
.build() .build()
}.apply {
Timber.d("Static map image request: ${this?.data}")
} }
) )
if (painter.state is AsyncImagePainter.State.Success) { val collectedState = painter.state.collectAsState()
if (collectedState.value is AsyncImagePainter.State.Success) {
Image( Image(
painter = painter, painter = painter,
contentDescription = contentDescription, contentDescription = contentDescription,
@ -100,7 +102,7 @@ fun StaticMapView(
) )
} else { } else {
StaticMapPlaceholder( StaticMapPlaceholder(
showProgress = painter.state is AsyncImagePainter.State.Loading, showProgress = collectedState.value.isLoading(),
contentDescription = contentDescription, contentDescription = contentDescription,
width = maxWidth, width = maxWidth,
height = maxHeight, height = maxHeight,
@ -110,6 +112,11 @@ fun StaticMapView(
} }
} }
private fun AsyncImagePainter.State.isLoading(): Boolean {
return this is AsyncImagePainter.State.Empty ||
this is AsyncImagePainter.State.Loading
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun StaticMapViewPreview() = ElementPreview { internal fun StaticMapViewPreview() = ElementPreview {

View file

@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction

View file

@ -37,8 +37,8 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow

View file

@ -25,8 +25,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.ProtectedView

View file

@ -40,8 +40,8 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent

View file

@ -50,7 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction

View file

@ -63,6 +63,9 @@ dependencies {
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
api(projects.features.preferences.api) api(projects.features.preferences.api)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime) testImplementation(libs.molecule.runtime)

View file

@ -5,13 +5,10 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalCoilApi::class)
package io.element.android.features.preferences.impl.tasks package io.element.android.features.preferences.impl.tasks
import android.content.Context import android.content.Context
import coil.Coil import coil3.SingletonImageLoader
import coil.annotation.ExperimentalCoilApi
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
@ -43,7 +40,7 @@ class DefaultClearCacheUseCase @Inject constructor(
// Clear Matrix cache // Clear Matrix cache
matrixClient.clearCache() matrixClient.clearCache()
// Clear Coil cache // Clear Coil cache
Coil.imageLoader(context).let { SingletonImageLoader.get(context).let {
it.diskCache?.clear() it.diskCache?.clear()
it.memoryCache?.clear() it.memoryCache?.clear()
} }

View file

@ -28,8 +28,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.request.ImageRequest import coil3.request.ImageRequest
import io.element.android.features.rageshake.impl.R import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView

View file

@ -42,7 +42,7 @@ datetime = "0.6.2"
serialization_json = "1.8.0" serialization_json = "1.8.0"
#other #other
coil = "2.7.0" coil = "3.1.0"
showkase = "1.0.3" showkase = "1.0.3"
appyx = "1.6.0" appyx = "1.6.0"
sqldelight = "2.0.2" sqldelight = "2.0.2"
@ -159,10 +159,11 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "
test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.5.1" test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.5.1"
# Others # Others
coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" } coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" }
compound = { module = "io.element.android:compound-android", version = "25.2.26" } compound = { module = "io.element.android:compound-android", version = "25.2.26" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.designsystem.components
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Build import android.os.Build
import android.text.TextPaint import android.text.TextPaint
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
@ -85,9 +84,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize import androidx.compose.ui.unit.toSize
import coil.imageLoader import coil3.SingletonImageLoader
import coil.request.DefaultRequestOptions import coil3.request.ImageRequest
import coil.request.ImageRequest import coil3.request.allowHardware
import coil3.toBitmap
import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.vanniktech.blurhash.BlurHash import com.vanniktech.blurhash.BlurHash
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -328,7 +328,7 @@ fun Modifier.avatarBloom(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(avatarData) .data(avatarData)
// Allow cache and default dispatchers // Allow cache and default dispatchers
.defaults(DefaultRequestOptions()) .defaults(ImageRequest.Defaults())
// Needed to be able to read pixels from the Bitmap for the hash // Needed to be able to read pixels from the Bitmap for the hash
.allowHardware(false) .allowHardware(false)
// Reduce size so it loads faster for large avatars // Reduce size so it loads faster for large avatars
@ -340,9 +340,11 @@ fun Modifier.avatarBloom(
var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) } var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) }
LaunchedEffect(avatarData) { LaunchedEffect(avatarData) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val drawable = val bitmap = SingletonImageLoader.get(context)
context.imageLoader.execute(painterRequest).drawable ?: return@withContext .execute(painterRequest)
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext .image
?.toBitmap()
?: return@withContext
blurHash = BlurHash.encode( blurHash = BlurHash.encode(
bitmap = bitmap, bitmap = bitmap,
componentX = BloomDefaults.HASH_COMPONENTS, componentX = BloomDefaults.HASH_COMPONENTS,

View file

@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -26,10 +28,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent import coil3.compose.SubcomposeAsyncImageContent
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
@ -89,7 +91,8 @@ private fun ImageAvatar(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = modifier modifier = modifier
) { ) {
when (val state = painter.state) { val collectedState by painter.state.collectAsState()
when (val state = collectedState) {
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
is AsyncImagePainter.State.Error -> { is AsyncImagePainter.State.Error -> {
SideEffect { SideEffect {

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage import coil3.compose.AsyncImage
@Composable @Composable
fun BlurHashAsyncImage( fun BlurHashAsyncImage(

View file

@ -9,9 +9,14 @@ package io.element.android.libraries.designsystem.preview
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
@OptIn(ExperimentalCoilApi::class)
@Composable @Composable
@Suppress("ModifierMissing") @Suppress("ModifierMissing")
fun ElementPreview( fun ElementPreview(
@ -19,12 +24,14 @@ fun ElementPreview(
showBackground: Boolean = true, showBackground: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
ElementTheme(darkTheme = darkTheme) { CompositionLocalProvider(LocalAsyncImagePreviewHandler provides AsyncImagePreviewHandler { null }) {
if (showBackground) { ElementTheme(darkTheme = darkTheme) {
// If we have a proper contentColor applied we need a Surface instead of a Box if (showBackground) {
Surface(content = content) // If we have a proper contentColor applied we need a Surface instead of a Box
} else { Surface(content = content)
content() } else {
content()
}
} }
} }
} }

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.gif) implementation(libs.coil.gif)
implementation(libs.coil.network.okhttp)
implementation(libs.jsoup) implementation(libs.jsoup)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)

View file

@ -24,8 +24,8 @@ import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.request.ImageRequest import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight

View file

@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader import coil3.ImageLoader
import coil.fetch.Fetcher import coil3.fetch.Fetcher
import coil.request.Options import coil3.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -24,7 +24,6 @@ internal class AvatarDataFetcherFactory(
return CoilMediaFetcher( return CoilMediaFetcher(
mediaLoader = client.mediaLoader, mediaLoader = client.mediaLoader,
mediaData = data.toMediaRequestData(), mediaData = data.toMediaRequestData(),
options = options
) )
} }
} }

View file

@ -7,16 +7,16 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.decode.DataSource import coil3.decode.DataSource
import coil.decode.ImageSource import coil3.decode.ImageSource
import coil.fetch.FetchResult import coil3.fetch.FetchResult
import coil.fetch.Fetcher import coil3.fetch.Fetcher
import coil.fetch.SourceResult import coil3.fetch.SourceFetchResult
import coil.request.Options
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.toFile import io.element.android.libraries.matrix.api.media.toFile
import okio.Buffer import okio.Buffer
import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import timber.log.Timber import timber.log.Timber
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -24,7 +24,6 @@ import java.nio.ByteBuffer
internal class CoilMediaFetcher( internal class CoilMediaFetcher(
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val mediaData: MediaRequestData, private val mediaData: MediaRequestData,
private val options: Options
) : Fetcher { ) : Fetcher {
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
if (mediaData.source == null) { if (mediaData.source == null) {
@ -32,8 +31,8 @@ internal class CoilMediaFetcher(
return null return null
} }
return when (mediaData.kind) { return when (mediaData.kind) {
is MediaRequestData.Kind.Content -> fetchContent(mediaData.source, options) is MediaRequestData.Kind.Content -> fetchContent(mediaData.source)
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind, options) is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind)
is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind) is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind)
} }
} }
@ -47,8 +46,12 @@ internal class CoilMediaFetcher(
return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName) return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName)
.map { mediaFile -> .map { mediaFile ->
val file = mediaFile.toFile() val file = mediaFile.toFile()
SourceResult( SourceFetchResult(
source = ImageSource(file = file.toOkioPath(), closeable = mediaFile), source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
closeable = mediaFile,
),
mimeType = null, mimeType = null,
dataSource = DataSource.DISK dataSource = DataSource.DISK
) )
@ -59,37 +62,40 @@ internal class CoilMediaFetcher(
.getOrNull() .getOrNull()
} }
private suspend fun fetchContent(mediaSource: MediaSource, options: Options): FetchResult? { private suspend fun fetchContent(mediaSource: MediaSource): FetchResult? {
return mediaLoader.loadMediaContent( return mediaLoader.loadMediaContent(
source = mediaSource, source = mediaSource,
).map { byteArray -> ).map { byteArray ->
byteArray.asSourceResult(options) byteArray.asSourceResult()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
}.getOrNull() }.getOrNull()
} }
private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? { private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail): FetchResult? {
return mediaLoader.loadMediaThumbnail( return mediaLoader.loadMediaThumbnail(
source = mediaSource, source = mediaSource,
width = kind.width, width = kind.width,
height = kind.height, height = kind.height,
).map { byteArray -> ).map { byteArray ->
byteArray.asSourceResult(options) byteArray.asSourceResult()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
}.getOrNull() }.getOrNull()
} }
private fun ByteArray.asSourceResult(options: Options): SourceResult { private fun ByteArray.asSourceResult(): SourceFetchResult {
val byteBuffer = ByteBuffer.wrap(this) val byteBuffer = ByteBuffer.wrap(this)
val bufferedSource = try { val bufferedSource = try {
Buffer().apply { write(byteBuffer) } Buffer().apply { write(byteBuffer) }
} finally { } finally {
byteBuffer.position(0) byteBuffer.position(0)
} }
return SourceResult( return SourceFetchResult(
source = ImageSource(bufferedSource, options.context), source = ImageSource(
source = bufferedSource,
fileSystem = FileSystem.SYSTEM,
),
mimeType = null, mimeType = null,
dataSource = DataSource.MEMORY dataSource = DataSource.MEMORY
) )

View file

@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.ui.media
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import coil.ImageLoader import coil3.ImageLoader
import coil.ImageLoaderFactory import coil3.gif.AnimatedImageDecoder
import coil.decode.GifDecoder import coil3.gif.GifDecoder
import coil.decode.ImageDecoderDecoder import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
@ -31,13 +31,19 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
) : LoggedInImageLoaderFactory { ) : LoggedInImageLoaderFactory {
override fun newImageLoader(matrixClient: MatrixClient): ImageLoader { override fun newImageLoader(matrixClient: MatrixClient): ImageLoader {
return ImageLoader return ImageLoader.Builder(context)
.Builder(context)
.okHttpClient { okHttpClient.get() }
.components { .components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
// Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient
okHttpClient.get().newBuilder().build()
}
)
)
// Add gif support // Add gif support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory()) add(AnimatedImageDecoder.Factory())
} else { } else {
add(GifDecoder.Factory()) add(GifDecoder.Factory())
} }
@ -53,11 +59,19 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
class NotLoggedInImageLoaderFactory @Inject constructor( class NotLoggedInImageLoaderFactory @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
) : ImageLoaderFactory { ) {
override fun newImageLoader(): ImageLoader { fun newImageLoader(): ImageLoader {
return ImageLoader return ImageLoader.Builder(context)
.Builder(context) .components {
.okHttpClient { okHttpClient.get() } add(
OkHttpNetworkFetcherFactory(
callFactory = {
// Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient
okHttpClient.get().newBuilder().build()
}
)
)
}
.build() .build()
} }
} }

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
/** /**
* Can be use with [coil.compose.AsyncImage] to load a [MediaSource]. * Can be use with [coil3.compose.AsyncImage] to load a [MediaSource].
* This will go internally through our [CoilMediaFetcher]. * This will go internally through our [CoilMediaFetcher].
* *
* Example of usage: * Example of usage:

View file

@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader import coil3.ImageLoader
import coil.fetch.Fetcher import coil3.fetch.Fetcher
import coil.request.Options import coil3.request.Options
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
internal class MediaRequestDataFetcherFactory( internal class MediaRequestDataFetcherFactory(
@ -23,7 +23,6 @@ internal class MediaRequestDataFetcherFactory(
return CoilMediaFetcher( return CoilMediaFetcher(
mediaLoader = client.mediaLoader, mediaLoader = client.mediaLoader,
mediaData = data, mediaData = data,
options = options
) )
} }
} }

View file

@ -7,8 +7,8 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.key.Keyer import coil3.key.Keyer
import coil.request.Options import coil3.request.Options
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
internal class AvatarDataKeyer : Keyer<AvatarData> { internal class AvatarDataKeyer : Keyer<AvatarData> {

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import coil.ImageLoader import coil3.ImageLoader
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
class FakeLoggedInImageLoaderFactory( class FakeLoggedInImageLoaderFactory(

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon

View file

@ -23,8 +23,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight

View file

@ -30,8 +30,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -46,7 +46,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.api.notifications
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader import coil3.ImageLoader
interface NotificationBitmapLoader { interface NotificationBitmapLoader {
/** /**

View file

@ -11,10 +11,11 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap import coil3.ImageLoader
import coil.ImageLoader import coil3.request.ImageRequest
import coil.request.ImageRequest import coil3.request.transformations
import coil.transform.CircleCropTransformation import coil3.toBitmap
import coil3.transform.CircleCropTransformation
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
@ -50,7 +51,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.build() .build()
val result = imageLoader.execute(imageRequest) val result = imageLoader.execute(imageRequest)
result.drawable?.toBitmap() result.image?.toBitmap()
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Unable to load room bitmap") Timber.e(e, "Unable to load room bitmap")
null null
@ -78,7 +79,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.build() .build()
val result = imageLoader.execute(imageRequest) val result = imageLoader.execute(imageRequest)
val bitmap = result.drawable?.toBitmap() val bitmap = result.image?.toBitmap()
return bitmap?.let { IconCompat.createWithBitmap(it) } return bitmap?.let { IconCompat.createWithBitmap(it) }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Unable to load user bitmap") Timber.e(e, "Unable to load user bitmap")

View file

@ -12,7 +12,7 @@ import android.graphics.Typeface
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import coil.ImageLoader import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.NotificationIdProvider

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import android.graphics.Bitmap import android.graphics.Bitmap
import coil.ImageLoader import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId

View file

@ -16,7 +16,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person import androidx.core.app.Person
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import coil.ImageLoader import coil3.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.NotificationConfig import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification import android.app.Notification
import android.graphics.Bitmap import android.graphics.Bitmap
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.NotificationDataFactory import io.element.android.libraries.push.impl.notifications.NotificationDataFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.OneShotNotification

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification import android.app.Notification
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator

View file

@ -9,12 +9,11 @@ package io.element.android.libraries.push.test.notifications
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import coil.ImageLoader import coil3.ImageLoader
import coil.annotation.ExperimentalCoilApi import coil3.test.FakeImageLoaderEngine
import coil.test.FakeImageLoaderEngine import coil3.test.intercept
import org.robolectric.RuntimeEnvironment import org.robolectric.RuntimeEnvironment
@OptIn(ExperimentalCoilApi::class)
class FakeImageLoader { class FakeImageLoader {
private val coilRequests = mutableListOf<Any>() private val coilRequests = mutableListOf<Any>()

View file

@ -7,7 +7,7 @@
package io.element.android.libraries.push.test.notifications package io.element.android.libraries.push.test.notifications
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.push.test.notifications.push
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
class FakeNotificationBitmapLoader( class FakeNotificationBitmapLoader(