diff --git a/app/build.gradle b/app/build.gradle index 519af5b858..fcc4097158 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,6 +95,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' + implementation 'io.coil-kt:coil:2.2.1' implementation 'com.jakewharton.timber:timber:5.0.1' debugImplementation "androidx.compose.ui:ui-tooling" debugImplementation "androidx.compose.ui:ui-test-manifest" diff --git a/app/src/main/java/io/element/android/x/ElementXApplication.kt b/app/src/main/java/io/element/android/x/ElementXApplication.kt index c3f8cd59c1..a2dd80299d 100644 --- a/app/src/main/java/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/java/io/element/android/x/ElementXApplication.kt @@ -1,14 +1,17 @@ package io.element.android.x import android.app.Application +import coil.ImageLoader +import coil.ImageLoaderFactory import com.airbnb.mvrx.Mavericks import io.element.android.x.matrix.MatrixInstance +import io.element.android.x.matrix.media.MediaFetcher import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.MainScope import kotlinx.coroutines.plus import timber.log.Timber -class ElementXApplication : Application() { +class ElementXApplication : Application(), ImageLoaderFactory { private val applicationScope = MainScope() + CoroutineName("ElementX Scope") @@ -18,4 +21,13 @@ class ElementXApplication : Application() { MatrixInstance.init(this, applicationScope) Mavericks.initialize(this) } + + override fun newImageLoader(): ImageLoader { + return ImageLoader + .Builder(this) + .components { + add(MediaFetcher.Factory(MatrixInstance.getInstance())) + } + .build() + } } \ No newline at end of file diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index b231114945..70e3455d1f 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":libraries:designsystem")) implementation(project(":libraries:textcomposer")) implementation(libs.mavericks.compose) + implementation(libs.coil.compose) implementation(libs.timber) implementation(libs.datetime) implementation(libs.accompanist.flowlayout) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt index 992091fa61..65b2333497 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt @@ -87,7 +87,16 @@ class MessageTimelineItemStateMapper( body = messageType.content.body, formattedBody = messageType.content.formatted ) - is MessageType.Image -> MessagesTimelineItemUnknownContent + is MessageType.Image -> { + MessagesTimelineItemImageContent( + body = messageType.content.body, + imageMeta = MediaResolver.Meta( + source = messageType.content.source, + kind = MediaResolver.Kind.Content + ), + blurhash = messageType.content.info?.blurhash + ) + } is MessageType.Notice -> MessagesTimelineItemNoticeContent( body = messageType.content.body, formattedBody = messageType.content.formatted diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index b524911c37..145ffc81b4 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -43,10 +43,7 @@ import io.element.android.x.features.messages.components.* import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesViewState -import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent -import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent +import io.element.android.x.features.messages.model.content.* import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel import io.element.android.x.features.messages.textcomposer.MessageComposerViewState import io.element.android.x.textcomposer.TextComposer @@ -302,6 +299,10 @@ fun MessageEventRow( content = messageEvent.content, modifier = contentModifier ) + is MessagesTimelineItemImageContent -> MessagesTimelineItemImageView( + content = messageEvent.content, + modifier = contentModifier + ) } } MessagesReactionsView( diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt new file mode 100644 index 0000000000..a9387bdfd6 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesTimelineItemImageView.kt @@ -0,0 +1,23 @@ +package io.element.android.x.features.messages.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage +import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent + +@Composable +fun MessagesTimelineItemImageView( + content: MessagesTimelineItemImageContent, + modifier: Modifier = Modifier +) { + Box(modifier) { + AsyncImage( + model = content.imageMeta, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier + ) + } +} \ No newline at end of file diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt new file mode 100644 index 0000000000..26e9fe50ea --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/content/MessagesTimelineItemImageContent.kt @@ -0,0 +1,9 @@ +package io.element.android.x.features.messages.model.content + +import io.element.android.x.matrix.media.MediaResolver + +data class MessagesTimelineItemImageContent( + val body: String, + val imageMeta: MediaResolver.Meta, + val blurhash: String? +) : MessagesTimelineItemContent \ No newline at end of file diff --git a/libraries/matrix/build.gradle.kts b/libraries/matrix/build.gradle.kts index 2520597b2e..e315786659 100644 --- a/libraries/matrix/build.gradle.kts +++ b/libraries/matrix/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":libraries:core")) implementation(libs.timber) implementation("net.java.dev.jna:jna:5.10.0@aar") + implementation(libs.coil.compose) implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) } \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt new file mode 100644 index 0000000000..010d42102a --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt @@ -0,0 +1,38 @@ +package io.element.android.x.matrix.media + +import coil.ImageLoader +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.request.Options +import io.element.android.x.matrix.Matrix +import java.nio.ByteBuffer + +class MediaFetcher( + private val mediaResolver: MediaResolver, + private val meta: MediaResolver.Meta, + private val options: Options, + private val imageLoader: ImageLoader +) : Fetcher { + + override suspend fun fetch(): FetchResult? { + val byteArray = mediaResolver.resolve(meta) ?: return null + val byteBuffer = ByteBuffer.wrap(byteArray) + return imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() + } + + class Factory(private val matrix: Matrix) : Fetcher.Factory { + override fun create( + data: MediaResolver.Meta, + options: Options, + imageLoader: ImageLoader + ): Fetcher { + val activeClient = matrix.activeClient() + return MediaFetcher( + mediaResolver = activeClient.mediaResolver(), + meta = data, + options = options, + imageLoader = imageLoader + ) + } + } +} \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaResolver.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaResolver.kt index c72000290f..949ce02cf4 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaResolver.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaResolver.kt @@ -1,6 +1,7 @@ package io.element.android.x.matrix.media import io.element.android.x.matrix.MatrixClient +import org.matrix.rustcomponents.sdk.MediaSource import org.matrix.rustcomponents.sdk.mediaSourceFromUrl interface MediaResolver { @@ -13,7 +14,14 @@ interface MediaResolver { object Content : Kind } + data class Meta( + val source: MediaSource, + val kind: Kind + ) + suspend fun resolve(url: String?, kind: Kind): ByteArray? + + suspend fun resolve(meta: Meta): ByteArray? } @@ -22,12 +30,16 @@ internal class RustMediaResolver(private val client: MatrixClient) : MediaResolv override suspend fun resolve(url: String?, kind: MediaResolver.Kind): ByteArray? { if (url.isNullOrEmpty()) return null val mediaSource = mediaSourceFromUrl(url) - return when (kind) { - is MediaResolver.Kind.Content -> client.loadMediaContentForSource(mediaSource) + return resolve(MediaResolver.Meta(mediaSource, kind)) + } + + override suspend fun resolve(meta: MediaResolver.Meta): ByteArray? { + return when (meta.kind) { + is MediaResolver.Kind.Content -> client.loadMediaContentForSource(meta.source) is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnailForSource( - mediaSource, - kind.width.toLong(), - kind.height.toLong() + meta.source, + meta.kind.width.toLong(), + meta.kind.height.toLong() ) }.getOrNull() }