Let notifications use avatar fallback.
Extract code which handles Matrix image to its own api / impl / test modules.
This commit is contained in:
@@ -39,6 +39,7 @@ dependencies {
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.login.api)
|
||||
|
||||
@@ -33,7 +33,7 @@ import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
|
||||
import io.element.android.libraries.designsystem.utils.ScreenOrientation
|
||||
import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@@ -42,7 +42,7 @@ class NotLoggedInFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val loginEntryPoint: LoginEntryPoint,
|
||||
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
|
||||
private val imageLoaderHolder: ImageLoaderHolder,
|
||||
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
@@ -66,7 +66,7 @@ class NotLoggedInFlowNode(
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader())
|
||||
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.impl)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
@@ -94,7 +94,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.matrixuiTest)
|
||||
testImplementation(projects.libraries.matrixmedia.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
|
||||
@@ -22,6 +22,8 @@ import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver
|
||||
import io.element.android.features.call.impl.ui.IncomingCallActivity
|
||||
import io.element.android.features.call.impl.utils.IntentProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
@@ -70,7 +72,15 @@ class RingingCallNotificationCreator(
|
||||
): Notification? {
|
||||
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val imageLoader = imageLoaderHolder.get(matrixClient)
|
||||
val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader)
|
||||
val largeIcon = notificationBitmapLoader.getUserIcon(
|
||||
avatarData = AvatarData(
|
||||
id = roomId.value,
|
||||
name = roomName,
|
||||
url = roomAvatarUrl,
|
||||
size = AvatarSize.RoomDetailsHeader,
|
||||
),
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
|
||||
val caller = Person.Builder()
|
||||
.setName(senderDisplayName)
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import coil3.ImageLoader
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
@@ -53,7 +54,7 @@ class RingingCallNotificationCreatorTest {
|
||||
|
||||
@Test
|
||||
fun `createNotification - tries to load the avatar URL`() = runTest {
|
||||
val getUserIconLambda = lambdaRecorder<String?, ImageLoader, IconCompat?> { _, _ -> null }
|
||||
val getUserIconLambda = lambdaRecorder<AvatarData, ImageLoader, IconCompat?> { _, _ -> null }
|
||||
val notificationCreator = createRingingCallNotificationCreator(
|
||||
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
|
||||
notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda)
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.textcomposer.impl)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
@@ -46,7 +46,7 @@ internal fun ImageAvatar(
|
||||
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
SideEffect {
|
||||
Timber.Forest.e(
|
||||
Timber.e(
|
||||
state.result.throwable,
|
||||
"Error loading avatar $state\n${state.result}"
|
||||
)
|
||||
|
||||
@@ -11,11 +11,11 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.matrix.ui.test"
|
||||
namespace = "io.element.android.libraries.matrix.ui.media.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(libs.coil.compose)
|
||||
}
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
|
||||
/**
|
||||
* The size in pixel of the thumbnail to generate for the avatar.
|
||||
* This is not the size of the avatar displayed in the UI but the size to get from the servers.
|
||||
@@ -25,10 +22,3 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
* Let's always use the same size so coil caching works properly.
|
||||
*/
|
||||
const val AVATAR_THUMBNAIL_SIZE_IN_PIXEL = 240L
|
||||
|
||||
internal fun AvatarData.toMediaRequestData(): MediaRequestData {
|
||||
return MediaRequestData(
|
||||
source = url?.let { MediaSource(it) },
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import coil3.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface ImageLoaderHolder {
|
||||
fun get(): ImageLoader
|
||||
fun get(client: MatrixClient): ImageLoader
|
||||
fun remove(sessionId: SessionId)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
||||
/**
|
||||
* Generates a bitmap for an initials avatar based on the provided [io.element.android.libraries.designsystem.components.avatar.AvatarData].
|
||||
*/
|
||||
interface InitialsAvatarBitmapGenerator {
|
||||
fun generateBitmap(
|
||||
size: Int,
|
||||
avatarData: AvatarData,
|
||||
useDarkTheme: Boolean,
|
||||
fontSizePercentage: Float = 0.5f,
|
||||
): Bitmap?
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.media.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:
|
||||
* AsyncImage(
|
||||
33
libraries/matrixmedia/impl/build.gradle.kts
Normal file
33
libraries/matrixmedia/impl/build.gradle.kts
Normal file
@@ -0,0 +1,33 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.matrix.ui.media.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.gif)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
|
||||
internal fun AvatarData.toMediaRequestData(): MediaRequestData {
|
||||
return MediaRequestData(
|
||||
source = url?.let { MediaSource(it) },
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
)
|
||||
}
|
||||
@@ -27,14 +27,15 @@ internal class CoilMediaFetcher(
|
||||
private val mediaData: MediaRequestData,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
if (mediaData.source == null) {
|
||||
val source = mediaData.source
|
||||
if (source == null) {
|
||||
Timber.e("MediaData source is null")
|
||||
return null
|
||||
}
|
||||
return when (mediaData.kind) {
|
||||
is MediaRequestData.Kind.Content -> fetchContent(mediaData.source)
|
||||
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind)
|
||||
is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind)
|
||||
return when (val kind = mediaData.kind) {
|
||||
is MediaRequestData.Kind.Content -> fetchContent(source)
|
||||
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(source, kind)
|
||||
is MediaRequestData.Kind.File -> fetchFile(source, kind)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,16 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
|
||||
interface ImageLoaderHolder {
|
||||
fun get(client: MatrixClient): ImageLoader
|
||||
fun remove(sessionId: SessionId)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultImageLoaderHolder(
|
||||
private val loggedInImageLoaderFactory: LoggedInImageLoaderFactory,
|
||||
private val imageLoaderFactory: ImageLoaderFactory,
|
||||
private val sessionObserver: SessionObserver,
|
||||
) : ImageLoaderHolder {
|
||||
private val map = mutableMapOf<SessionId, ImageLoader>()
|
||||
private val notLoggedInImageLoader by lazy {
|
||||
imageLoaderFactory.newImageLoader()
|
||||
}
|
||||
|
||||
init {
|
||||
observeSessions()
|
||||
@@ -42,10 +40,14 @@ class DefaultImageLoaderHolder(
|
||||
})
|
||||
}
|
||||
|
||||
override fun get(): ImageLoader {
|
||||
return notLoggedInImageLoader
|
||||
}
|
||||
|
||||
override fun get(client: MatrixClient): ImageLoader {
|
||||
return synchronized(map) {
|
||||
map.getOrPut(client.sessionId) {
|
||||
loggedInImageLoaderFactory
|
||||
imageLoaderFactory
|
||||
.newImageLoader(client.matrixMediaLoader)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.createBitmap
|
||||
import coil3.Bitmap
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.compound.theme.AvatarColors
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
@@ -35,60 +37,35 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
/**
|
||||
* Generates a bitmap for an initials avatar based on the provided [AvatarData].
|
||||
*/
|
||||
class InitialsAvatarBitmapGenerator(
|
||||
useDarkTheme: Boolean = false,
|
||||
private val fontSizePercentage: Float = 0.5f,
|
||||
) {
|
||||
private val compoundColors: SemanticColors = if (useDarkTheme) {
|
||||
compoundColorsDark
|
||||
} else {
|
||||
compoundColorsLight
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultInitialsAvatarBitmapGenerator : InitialsAvatarBitmapGenerator {
|
||||
// List of predefined avatar colors to use for initials avatars, in light mode
|
||||
private val allAvatarColors: List<AvatarColors> = listOf(
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative1,
|
||||
foreground = compoundColors.textDecorative1,
|
||||
),
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative2,
|
||||
foreground = compoundColors.textDecorative2,
|
||||
),
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative3,
|
||||
foreground = compoundColors.textDecorative3,
|
||||
),
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative4,
|
||||
foreground = compoundColors.textDecorative4,
|
||||
),
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative5,
|
||||
foreground = compoundColors.textDecorative5,
|
||||
),
|
||||
AvatarColors(
|
||||
background = compoundColors.bgDecorative6,
|
||||
foreground = compoundColors.textDecorative6,
|
||||
),
|
||||
)
|
||||
private val lightAvatarColors: List<AvatarColors> = compoundColorsLight.buildAvatarColors()
|
||||
|
||||
// List of predefined avatar colors to use for initials avatars, in dark mode
|
||||
private val darkAvatarColors: List<AvatarColors> = compoundColorsDark.buildAvatarColors()
|
||||
|
||||
/**
|
||||
* Generates a bitmap for an avatar with no URL, using the initials from the [AvatarData].
|
||||
* @param size The size of the bitmap to generate, in pixels.
|
||||
* @param avatarData The [AvatarData] containing the initials and other information.
|
||||
* @param useDarkTheme Whether the theme is dark.
|
||||
* @param fontSizePercentage The percentage of the avatar size to use for the font size.
|
||||
*/
|
||||
fun generateBitmap(size: Int, avatarData: AvatarData): Bitmap? {
|
||||
override fun generateBitmap(
|
||||
size: Int,
|
||||
avatarData: AvatarData,
|
||||
useDarkTheme: Boolean,
|
||||
fontSizePercentage: Float,
|
||||
): Bitmap? {
|
||||
if (avatarData.url != null) {
|
||||
// This generator is only for initials avatars, not for avatars with URLs
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the color pair to use for the initials avatar
|
||||
val avatarColors = allAvatarColors[avatarData.id.sumOf { it.code } % allAvatarColors.size]
|
||||
val colors = if (useDarkTheme) darkAvatarColors else lightAvatarColors
|
||||
val avatarColors = colors[avatarData.id.sumOf { it.code } % colors.size]
|
||||
|
||||
val bitmap = createBitmap(size, size)
|
||||
Canvas(bitmap).run {
|
||||
@@ -116,20 +93,32 @@ class InitialsAvatarBitmapGenerator(
|
||||
}
|
||||
}
|
||||
|
||||
private fun SemanticColors.buildAvatarColors(): List<AvatarColors> = listOf(
|
||||
AvatarColors(background = bgDecorative1, foreground = textDecorative1),
|
||||
AvatarColors(background = bgDecorative2, foreground = textDecorative2),
|
||||
AvatarColors(background = bgDecorative3, foreground = textDecorative3),
|
||||
AvatarColors(background = bgDecorative4, foreground = textDecorative4),
|
||||
AvatarColors(background = bgDecorative5, foreground = textDecorative5),
|
||||
AvatarColors(background = bgDecorative6, foreground = textDecorative6),
|
||||
)
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun InitialsAvatarBitmapGeneratorPreview() = ElementPreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
val generator = remember { DefaultInitialsAvatarBitmapGenerator() }
|
||||
repeat(6) { index ->
|
||||
val avatarData = remember { AvatarData(id = index.toString(), name = Char('0'.code + index).toString(), size = AvatarSize.IncomingCall) }
|
||||
val isLightTheme = ElementTheme.isLightTheme
|
||||
val bitmap = remember(isLightTheme) {
|
||||
val generator = InitialsAvatarBitmapGenerator(useDarkTheme = !isLightTheme)
|
||||
generator.generateBitmap(512, avatarData)?.asImageBitmap()
|
||||
generator.generateBitmap(
|
||||
size = 512,
|
||||
avatarData = avatarData,
|
||||
useDarkTheme = !isLightTheme,
|
||||
)?.asImageBitmap()
|
||||
}
|
||||
|
||||
bitmap?.let {
|
||||
Image(bitmap = it, contentDescription = null, modifier = Modifier.size(48.dp))
|
||||
} ?: Text("No avatar generated")
|
||||
@@ -16,32 +16,40 @@ import coil3.gif.GifDecoder
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.Provider
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
interface LoggedInImageLoaderFactory {
|
||||
interface ImageLoaderFactory {
|
||||
fun newImageLoader(): ImageLoader
|
||||
fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLoggedInImageLoaderFactory(
|
||||
class DefaultImageLoaderFactory(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
) : LoggedInImageLoaderFactory {
|
||||
) : ImageLoaderFactory {
|
||||
private val okHttpNetworkFetcherFactory = OkHttpNetworkFetcherFactory(
|
||||
callFactory = {
|
||||
// Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient
|
||||
okHttpClient().newBuilder().build()
|
||||
}
|
||||
)
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(okHttpNetworkFetcherFactory)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(
|
||||
OkHttpNetworkFetcherFactory(
|
||||
callFactory = {
|
||||
// Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient
|
||||
okHttpClient().newBuilder().build()
|
||||
}
|
||||
)
|
||||
)
|
||||
add(okHttpNetworkFetcherFactory)
|
||||
// Add gif support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(AnimatedImageDecoder.Factory())
|
||||
@@ -56,24 +64,3 @@ class DefaultLoggedInImageLoaderFactory(
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
class NotLoggedInImageLoaderFactory(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
) {
|
||||
fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(
|
||||
OkHttpNetworkFetcherFactory(
|
||||
callFactory = {
|
||||
// Use newBuilder, see https://coil-kt.github.io/coil/network/#using-a-custom-okhttpclient
|
||||
okHttpClient().newBuilder().build()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,5 @@ internal class MediaRequestDataKeyer : Keyer<MediaRequestData> {
|
||||
}
|
||||
|
||||
private fun MediaRequestData.toKey(): String? {
|
||||
if (source == null) return null
|
||||
return "${source.url}_$kind"
|
||||
return source?.let { "${it.url}_$kind" }
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
|
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
@@ -30,9 +31,10 @@ class DefaultImageLoaderHolderTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val lambda = lambdaRecorder<MatrixMediaLoader, ImageLoader> { ImageLoader.Builder(context).build() }
|
||||
|
||||
val holder = DefaultImageLoaderHolder(
|
||||
loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda),
|
||||
sessionObserver = NoOpSessionObserver()
|
||||
val holder = createDefaultImageLoaderHolder(
|
||||
imageLoaderFactory = FakeImageLoaderFactory(
|
||||
newMatrixImageLoaderLambda = lambda,
|
||||
),
|
||||
)
|
||||
val client = FakeMatrixClient()
|
||||
val imageLoader1 = holder.get(client)
|
||||
@@ -50,8 +52,10 @@ class DefaultImageLoaderHolderTest {
|
||||
lambdaRecorder<MatrixMediaLoader, ImageLoader> { ImageLoader.Builder(context).build() }
|
||||
val sessionObserver = FakeSessionObserver()
|
||||
val holder = DefaultImageLoaderHolder(
|
||||
loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda),
|
||||
sessionObserver = sessionObserver
|
||||
imageLoaderFactory = FakeImageLoaderFactory(
|
||||
newMatrixImageLoaderLambda = lambda,
|
||||
),
|
||||
sessionObserver = sessionObserver,
|
||||
)
|
||||
assertThat(sessionObserver.listeners.size).isEqualTo(1)
|
||||
val client = FakeMatrixClient()
|
||||
@@ -69,10 +73,20 @@ class DefaultImageLoaderHolderTest {
|
||||
lambdaRecorder<MatrixMediaLoader, ImageLoader> { ImageLoader.Builder(context).build() }
|
||||
val sessionObserver = FakeSessionObserver()
|
||||
DefaultImageLoaderHolder(
|
||||
loggedInImageLoaderFactory = FakeLoggedInImageLoaderFactory(lambda),
|
||||
sessionObserver = sessionObserver
|
||||
imageLoaderFactory = FakeImageLoaderFactory(
|
||||
newMatrixImageLoaderLambda = lambda,
|
||||
),
|
||||
sessionObserver = sessionObserver,
|
||||
)
|
||||
assertThat(sessionObserver.listeners.size).isEqualTo(1)
|
||||
sessionObserver.onSessionCreated(A_SESSION_ID.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultImageLoaderHolder(
|
||||
imageLoaderFactory: ImageLoaderFactory = FakeImageLoaderFactory(),
|
||||
sessionObserver: SessionObserver = NoOpSessionObserver(),
|
||||
) = DefaultImageLoaderHolder(
|
||||
imageLoaderFactory = imageLoaderFactory,
|
||||
sessionObserver = sessionObserver,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import coil3.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeImageLoaderFactory(
|
||||
private val newImageLoaderLambda: () -> ImageLoader = { lambdaError() },
|
||||
private val newMatrixImageLoaderLambda: (MatrixMediaLoader) -> ImageLoader = { lambdaError() },
|
||||
) : ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return newImageLoaderLambda()
|
||||
}
|
||||
|
||||
override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader {
|
||||
return newMatrixImageLoaderLambda(matrixMediaLoader)
|
||||
}
|
||||
}
|
||||
22
libraries/matrixmedia/test/build.gradle.kts
Normal file
22
libraries/matrixmedia/test/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.matrix.ui.media.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.coil.compose)
|
||||
}
|
||||
@@ -16,6 +16,10 @@ import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
class FakeImageLoaderHolder(
|
||||
val fakeImageLoader: ImageLoader = FakeImageLoader(),
|
||||
) : ImageLoaderHolder {
|
||||
override fun get(): ImageLoader {
|
||||
return fakeImageLoader
|
||||
}
|
||||
|
||||
override fun get(client: MatrixClient): ImageLoader {
|
||||
return fakeImageLoader
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.test.media
|
||||
|
||||
import coil3.Bitmap
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerator
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeInitialsAvatarBitmapGenerator(
|
||||
private val generateBitmapResult: (Int, AvatarData, Boolean, Float) -> Bitmap? = { _, _, _, _ -> lambdaError() }
|
||||
) : InitialsAvatarBitmapGenerator {
|
||||
override fun generateBitmap(
|
||||
size: Int,
|
||||
avatarData: AvatarData,
|
||||
useDarkTheme: Boolean,
|
||||
fontSizePercentage: Float,
|
||||
): Bitmap? {
|
||||
return generateBitmapResult(size, avatarData, useDarkTheme, fontSizePercentage)
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,12 @@ dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.gif)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
implementation(libs.jsoup)
|
||||
implementation(projects.libraries.previewutils)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import coil3.ImageLoader
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
|
||||
class FakeLoggedInImageLoaderFactory(
|
||||
private val newImageLoaderLambda: (MatrixMediaLoader) -> ImageLoader
|
||||
) : LoggedInImageLoaderFactory {
|
||||
override fun newImageLoader(matrixMediaLoader: MatrixMediaLoader): ImageLoader {
|
||||
return newImageLoaderLambda(matrixMediaLoader)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.voiceplayer.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
@@ -61,6 +61,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.matrixui)
|
||||
testImplementation(projects.libraries.mediaviewer.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
|
||||
@@ -17,7 +17,8 @@ dependencies {
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
}
|
||||
|
||||
@@ -11,17 +11,18 @@ package io.element.android.libraries.push.api.notifications
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import coil3.ImageLoader
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
|
||||
|
||||
interface NotificationBitmapLoader {
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* @param path mxc url
|
||||
* @param avatarData the data related to the Avatar
|
||||
* @param imageLoader Coil image loader
|
||||
* @param targetSize The size we want the bitmap to be resized to
|
||||
*/
|
||||
suspend fun getRoomBitmap(
|
||||
path: String?,
|
||||
avatarData: AvatarData,
|
||||
imageLoader: ImageLoader,
|
||||
targetSize: Long = AVATAR_THUMBNAIL_SIZE_IN_PIXEL,
|
||||
): Bitmap?
|
||||
@@ -29,8 +30,11 @@ interface NotificationBitmapLoader {
|
||||
/**
|
||||
* Get icon of a user.
|
||||
* Before Android P, this does nothing because the icon won't be used
|
||||
* @param path mxc url
|
||||
* @param avatarData the data related to the Avatar
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat?
|
||||
suspend fun getUserIcon(
|
||||
avatarData: AvatarData,
|
||||
imageLoader: ImageLoader,
|
||||
): IconCompat?
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ dependencies {
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrixmedia.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
@@ -77,7 +78,7 @@ dependencies {
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coil.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.matrixuiTest)
|
||||
testImplementation(projects.libraries.matrixmedia.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
@@ -19,9 +20,11 @@ import coil3.toBitmap
|
||||
import coil3.transform.CircleCropTransformation
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
|
||||
import io.element.android.libraries.matrix.ui.media.InitialsAvatarBitmapGenerator
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
@@ -31,48 +34,71 @@ import timber.log.Timber
|
||||
class DefaultNotificationBitmapLoader(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
private val initialsAvatarBitmapGenerator: InitialsAvatarBitmapGenerator,
|
||||
) : NotificationBitmapLoader {
|
||||
override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader, targetSize: Long): Bitmap? {
|
||||
if (path == null) {
|
||||
return null
|
||||
}
|
||||
return loadRoomBitmap(path, imageLoader, targetSize)
|
||||
}
|
||||
|
||||
private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader, targetSize: Long): Bitmap? {
|
||||
override suspend fun getRoomBitmap(
|
||||
avatarData: AvatarData,
|
||||
imageLoader: ImageLoader,
|
||||
targetSize: Long,
|
||||
): Bitmap? {
|
||||
return try {
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(targetSize)))
|
||||
.transformations(CircleCropTransformation())
|
||||
.build()
|
||||
val result = imageLoader.execute(imageRequest)
|
||||
result.image?.toBitmap()
|
||||
loadBitmap(
|
||||
avatarData = avatarData,
|
||||
imageLoader = imageLoader,
|
||||
targetSize = targetSize,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load room bitmap")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||
override suspend fun getUserIcon(
|
||||
avatarData: AvatarData,
|
||||
imageLoader: ImageLoader,
|
||||
): IconCompat? {
|
||||
if (sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
||||
return loadUserIcon(path, imageLoader)
|
||||
}
|
||||
|
||||
private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? {
|
||||
return try {
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)))
|
||||
.transformations(CircleCropTransformation())
|
||||
.build()
|
||||
val result = imageLoader.execute(imageRequest)
|
||||
val bitmap = result.image?.toBitmap()
|
||||
return bitmap?.let { IconCompat.createWithBitmap(it) }
|
||||
loadBitmap(
|
||||
avatarData = avatarData,
|
||||
imageLoader = imageLoader,
|
||||
targetSize = AVATAR_THUMBNAIL_SIZE_IN_PIXEL,
|
||||
)
|
||||
?.let { IconCompat.createWithBitmap(it) }
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load user bitmap")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDarkTheme(): Boolean {
|
||||
return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
|
||||
private suspend fun loadBitmap(
|
||||
avatarData: AvatarData,
|
||||
imageLoader: ImageLoader,
|
||||
targetSize: Long
|
||||
): Bitmap? {
|
||||
val path = avatarData.url
|
||||
val data = if (path != null) {
|
||||
MediaRequestData(
|
||||
source = MediaSource(path),
|
||||
kind = MediaRequestData.Kind.Thumbnail(targetSize),
|
||||
)
|
||||
} else {
|
||||
initialsAvatarBitmapGenerator.generateBitmap(
|
||||
size = targetSize.toInt(),
|
||||
avatarData = avatarData,
|
||||
useDarkTheme = isDarkTheme(),
|
||||
)
|
||||
}
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.transformations(CircleCropTransformation())
|
||||
.build()
|
||||
return imageLoader.execute(imageRequest).image?.toBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import android.graphics.Bitmap
|
||||
import coil3.ImageLoader
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
@@ -90,7 +92,18 @@ class DefaultRoomGroupMessageCreator(
|
||||
imageLoader: ImageLoader,
|
||||
): Bitmap? {
|
||||
// Use the last event (most recent?)
|
||||
return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath }
|
||||
?.let { bitmapLoader.getRoomBitmap(it, imageLoader) }
|
||||
val event = events.reversed().firstOrNull { it.roomAvatarPath != null }
|
||||
?: events.reversed().firstOrNull()
|
||||
return event?.let { event ->
|
||||
bitmapLoader.getRoomBitmap(
|
||||
avatarData = AvatarData(
|
||||
id = event.roomId.value,
|
||||
name = event.roomName,
|
||||
url = event.roomAvatarPath,
|
||||
size = AvatarSize.RoomDetailsHeader,
|
||||
),
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications.conversations
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
@@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.InitialsAvatarBitmapGenerator
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
||||
import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||
@@ -95,15 +93,16 @@ class DefaultNotificationConversationService(
|
||||
val imageLoader = imageLoaderHolder.get(client)
|
||||
|
||||
val defaultShortcutIconSize = ShortcutManagerCompat.getIconMaxWidth(context)
|
||||
val useDarkTheme = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
val icon = bitmapLoader.getRoomBitmap(
|
||||
path = roomAvatarUrl,
|
||||
avatarData = AvatarData(
|
||||
id = roomId.value,
|
||||
name = roomName,
|
||||
url = roomAvatarUrl,
|
||||
size = AvatarSize.RoomDetailsHeader,
|
||||
),
|
||||
imageLoader = imageLoader,
|
||||
targetSize = defaultShortcutIconSize.toLong()
|
||||
)?.let(IconCompat::createWithBitmap)
|
||||
?: InitialsAvatarBitmapGenerator(useDarkTheme = useDarkTheme)
|
||||
.generateBitmap(defaultShortcutIconSize, AvatarData(id = roomId.value, name = roomName, size = AvatarSize.RoomDetailsHeader))
|
||||
?.let(IconCompat::createWithAdaptiveBitmap)
|
||||
|
||||
val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId))
|
||||
.setShortLabel(roomName)
|
||||
|
||||
@@ -20,12 +20,15 @@ import coil3.ImageLoader
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.R
|
||||
@@ -401,7 +404,17 @@ class DefaultNotificationCreator(
|
||||
}
|
||||
Person.Builder()
|
||||
.setName(displayName.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
|
||||
.setIcon(
|
||||
bitmapLoader.getUserIcon(
|
||||
avatarData = AvatarData(
|
||||
id = event.senderId.value,
|
||||
name = senderName,
|
||||
url = event.senderAvatarPath,
|
||||
size = AvatarSize.UserHeader,
|
||||
),
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
)
|
||||
.setKey(key)
|
||||
.build()
|
||||
}
|
||||
@@ -460,7 +473,12 @@ class DefaultNotificationCreator(
|
||||
Person.Builder()
|
||||
// Note: name cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||
.setName(user.getBestName().annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
|
||||
.setIcon(
|
||||
bitmapLoader.getUserIcon(
|
||||
avatarData = user.getAvatarData(AvatarSize.UserHeader),
|
||||
imageLoader = imageLoader,
|
||||
)
|
||||
)
|
||||
.setKey(user.userId.value)
|
||||
.build()
|
||||
).also {
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
|
||||
import io.element.android.libraries.matrix.ui.test.media.FakeInitialsAvatarBitmapGenerator
|
||||
import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE
|
||||
import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE
|
||||
import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams
|
||||
@@ -232,6 +233,7 @@ fun createRoomGroupMessageCreator(
|
||||
val bitmapLoader = DefaultNotificationBitmapLoader(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
sdkIntProvider = sdkIntProvider,
|
||||
initialsAvatarBitmapGenerator = FakeInitialsAvatarBitmapGenerator(),
|
||||
)
|
||||
return DefaultRoomGroupMessageCreator(
|
||||
notificationCreator = createNotificationCreator(bitmapLoader = bitmapLoader),
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
|
||||
import io.element.android.libraries.matrix.ui.test.media.FakeInitialsAvatarBitmapGenerator
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||
@@ -297,7 +298,11 @@ fun createNotificationCreator(
|
||||
context: Context = RuntimeEnvironment.getApplication(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationChannels: NotificationChannels = createNotificationChannels(),
|
||||
bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
|
||||
bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(
|
||||
context = context,
|
||||
sdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R),
|
||||
initialsAvatarBitmapGenerator = FakeInitialsAvatarBitmapGenerator(),
|
||||
),
|
||||
): NotificationCreator {
|
||||
return DefaultNotificationCreator(
|
||||
context = context,
|
||||
|
||||
@@ -17,6 +17,7 @@ android {
|
||||
dependencies {
|
||||
api(projects.libraries.push.api)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.push.impl)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
|
||||
@@ -11,17 +11,18 @@ package io.element.android.libraries.push.test.notifications.push
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import coil3.ImageLoader
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
|
||||
class FakeNotificationBitmapLoader(
|
||||
var getRoomBitmapResult: (String?, ImageLoader, Long) -> Bitmap? = { _, _, _ -> null },
|
||||
var getUserIconResult: (String?, ImageLoader) -> IconCompat? = { _, _ -> null },
|
||||
var getRoomBitmapResult: (AvatarData, ImageLoader, Long) -> Bitmap? = { _, _, _ -> null },
|
||||
var getUserIconResult: (AvatarData, ImageLoader) -> IconCompat? = { _, _ -> null },
|
||||
) : NotificationBitmapLoader {
|
||||
override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader, targetSize: Long): Bitmap? {
|
||||
return getRoomBitmapResult(path, imageLoader, targetSize)
|
||||
override suspend fun getRoomBitmap(avatarData: AvatarData, imageLoader: ImageLoader, targetSize: Long): Bitmap? {
|
||||
return getRoomBitmapResult(avatarData, imageLoader, targetSize)
|
||||
}
|
||||
|
||||
override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
return getUserIconResult(path, imageLoader)
|
||||
override suspend fun getUserIcon(avatarData: AvatarData, imageLoader: ImageLoader): IconCompat? {
|
||||
return getUserIconResult(avatarData, imageLoader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:matrix:impl"))
|
||||
implementation(project(":libraries:matrixui"))
|
||||
implementation(project(":libraries:matrixmedia:impl"))
|
||||
implementation(project(":libraries:network"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:eventformatter:impl"))
|
||||
|
||||
Reference in New Issue
Block a user