Fix notification avatar

<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [x] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Use ImageLoader associated to a MatrixClient to be able to load avatars.
Create `ImageLoaderHolder` to share the `ImageHolder` between the one used by `LoggedInAppScopeFlowNode` and the one used by `DefaultNotificationDrawerManager`

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Fixes #1991

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->
NA

## Tests

<!-- Explain how you tested your development -->

- Log in to a session
- Kill the app
- Receive a message from someone with an avatar
- Observe that now the avatar is displayed in the notification.

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

User avatar are not always displayed in notification
Sometimes the letter is used instead.

Also the notifications are not rendering images, as it was the case on Element Android.

I'm on GHA debug build of 0a16af1d4ffc8911b949eb8c696f75dbbbe4f2c5, and it seems that if app is in recents, then the avatar images appear in notifications. If the app has been removed from recents, the images don't appear, even in subsequent notifications, and even after removing the first notification and receiving new notification for new messages from the same sender. Tested it a handful of times, occurs consistently on my device (Pixel 3, Android 13).
You're right @rom4nik , let's re-open this with this useful intel.

diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
index 7f36a7a51a5..423228c110d 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt
@@ -33,13 +33,12 @@ import dagger.assisted.AssistedInject
 import io.element.android.anvilannotations.ContributesNode
 import io.element.android.appnav.di.SessionComponentFactory
 import io.element.android.libraries.architecture.NodeInputs
-import io.element.android.libraries.architecture.bindings
 import io.element.android.libraries.architecture.createNode
 import io.element.android.libraries.architecture.inputs
 import io.element.android.libraries.di.AppScope
 import io.element.android.libraries.di.DaggerComponentOwner
 import io.element.android.libraries.matrix.api.MatrixClient
-import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
+import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
 import kotlinx.parcelize.Parcelize

 /**
@@ -52,6 +51,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
     @Assisted buildContext: BuildContext,
     @Assisted plugins: List<Plugin>,
     sessionComponentFactory: SessionComponentFactory,
+    private val imageLoaderHolder: ImageLoaderHolder,
 ) : ParentNode<LoggedInAppScopeFlowNode.NavTarget>(
     navModel = PermanentNavModel(
         navTargets = setOf(NavTarget),
@@ -78,8 +78,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
         super.onBuilt()
         lifecycle.subscribe(
             onCreate = {
-                val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
-                Coil.setImageLoader(imageLoaderFactory)
+                Coil.setImageLoader(imageLoaderHolder.get(inputs.matrixClient))
             },
         )
     }
diff --git a/changelog.d/1991.bugfix b/changelog.d/1991.bugfix
new file mode 100644
index 00000000000..e1dfcdcb2a7
--- /dev/null
+++ b/changelog.d/1991.bugfix
@@ -0,0 +1,1 @@
+Fix avatar not displayed in notification when the app is not in background
diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt
index cb695d4eda8..9f74b901421 100644
--- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt
+++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt
@@ -33,7 +33,7 @@ data class InviteListInviteSummary(
     val isNew: Boolean = false,
 )

-data class InviteSender constructor(
+data class InviteSender(
     val userId: UserId,
     val displayName: String,
     val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
index c1f01586059..64e4c117234 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt
@@ -16,7 +16,7 @@

 package io.element.android.features.login.impl.resolver

-data class HomeserverData constructor(
+data class HomeserverData(
     // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
     val homeserverUrl: String,
     // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index 2cf3d1f82c1..f7c157d2ba2 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -91,7 +91,7 @@ import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
 import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService

 @OptIn(ExperimentalCoroutinesApi::class)
-class RustMatrixClient constructor(
+class RustMatrixClient(
     private val client: Client,
     private val syncService: ClientSyncService,
     private val sessionStore: SessionStore,
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
index c495558fd93..4c5309e2bd2 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
@@ -28,8 +28,8 @@ import okhttp3.OkHttpClient
 import javax.inject.Inject
 import javax.inject.Provider

-class LoggedInImageLoaderFactory @Inject constructor(
-    @ApplicationContext private val context: Context,
+class LoggedInImageLoaderFactory(
+    private val context: Context,
     private val matrixClient: MatrixClient,
     private val okHttpClient: Provider<OkHttpClient>,
 ) : ImageLoaderFactory {
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt
new file mode 100644
index 00000000000..fc027d0e5c1
--- /dev/null
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.ui.media
+
+import android.content.Context
+import coil.ImageLoader
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.matrix.api.MatrixClient
+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
+import okhttp3.OkHttpClient
+import javax.inject.Inject
+import javax.inject.Provider
+
+interface ImageLoaderHolder {
+    fun get(client: MatrixClient): ImageLoader
+}
+
+@ContributesBinding(AppScope::class)
+@SingleIn(AppScope::class)
+class DefaultImageLoaderHolder @Inject constructor(
+    @ApplicationContext private val context: Context,
+    private val okHttpClient: Provider<OkHttpClient>,
+    private val sessionObserver: SessionObserver,
+) : ImageLoaderHolder {
+    private val map = mutableMapOf<SessionId, ImageLoader>()
+
+    init {
+        observeSessions()
+    }
+
+    private fun observeSessions() {
+        sessionObserver.addListener(object : SessionListener {
+            override suspend fun onSessionCreated(userId: String) = Unit
+
+            override suspend fun onSessionDeleted(userId: String) {
+                map.remove(SessionId(userId))
+            }
+        })
+    }
+
+    override fun get(client: MatrixClient): ImageLoader {
+        return synchronized(map) {
+            map.getOrPut(client.sessionId) {
+                LoggedInImageLoaderFactory(
+                    context = context,
+                    matrixClient = client,
+                    okHttpClient = okHttpClient,
+                ).newImageLoader()
+            }
+        }
+    }
+}
diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt
index 201fd9b0693..f3c186a34fc 100644
--- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt
+++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt
@@ -37,7 +37,7 @@ import java.util.UUID
 import javax.inject.Inject

 @ContributesBinding(AppScope::class)
-class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProvider {
+class PickerProviderImpl(private val isInTest: Boolean) : PickerProvider {

     @Inject
     constructor(): this(false)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
index 09f622e9787..904a6bf2ea3 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
 import io.element.android.libraries.matrix.api.core.SessionId
 import io.element.android.libraries.matrix.api.core.ThreadId
 import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
 import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
 import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
 import io.element.android.services.appnavstate.api.AppNavigationStateService
@@ -61,6 +62,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
     private val dispatchers: CoroutineDispatchers,
     private val buildMeta: BuildMeta,
     private val matrixClientProvider: MatrixClientProvider,
+    private val imageLoaderHolder: ImageLoaderHolder,
 ) : NotificationDrawerManager {
     private var appNavigationStateObserver: Job? = null

@@ -288,10 +290,11 @@ class DefaultNotificationDrawerManager @Inject constructor(
         }

         eventsForSessions.forEach { (sessionId, notifiableEvents) ->
+            val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
+            val imageLoader = imageLoaderHolder.get(client)
             val currentUser = tryOrNull(
                 onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
                 operation = {
-                    val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow()
                     // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
                     val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value
                     val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
@@ -307,7 +310,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
                 avatarUrl = null
             )

-            notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
+            notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
         }
     }
 }
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
index e78ce376398..4c1e3043ad0 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
@@ -21,7 +21,7 @@ import android.graphics.Bitmap
 import android.os.Build
 import androidx.core.graphics.drawable.IconCompat
 import androidx.core.graphics.drawable.toBitmap
-import coil.imageLoader
+import coil.ImageLoader
 import coil.request.ImageRequest
 import coil.transform.CircleCropTransformation
 import io.element.android.libraries.di.ApplicationContext
@@ -39,21 +39,22 @@ class NotificationBitmapLoader @Inject constructor(
     /**
      * Get icon of a room.
      * @param path mxc url
+     * @param imageLoader Coil image loader
      */
-    suspend fun getRoomBitmap(path: String?): Bitmap? {
+    suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
         if (path == null) {
             return null
         }
-        return loadRoomBitmap(path)
+        return loadRoomBitmap(path, imageLoader)
     }

-    private suspend fun loadRoomBitmap(path: String): Bitmap? {
+    private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader): Bitmap? {
         return try {
             val imageRequest = ImageRequest.Builder(context)
                 .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
                 .transformations(CircleCropTransformation())
                 .build()
-            val result = context.imageLoader.execute(imageRequest)
+            val result = imageLoader.execute(imageRequest)
             result.drawable?.toBitmap()
         } catch (e: Throwable) {
             Timber.e(e, "Unable to load room bitmap")
@@ -65,22 +66,23 @@ class NotificationBitmapLoader @Inject constructor(
      * Get icon of a user.
      * Before Android P, this does nothing because the icon won't be used
      * @param path mxc url
+     * @param imageLoader Coil image loader
      */
-    suspend fun getUserIcon(path: String?): IconCompat? {
+    suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
         if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
             return null
         }

-        return loadUserIcon(path)
+        return loadUserIcon(path, imageLoader)
     }

-    private suspend fun loadUserIcon(path: String): IconCompat? {
+    private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? {
         return try {
             val imageRequest = ImageRequest.Builder(context)
                 .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
                 .transformations(CircleCropTransformation())
                 .build()
-            val result = context.imageLoader.execute(imageRequest)
+            val result = imageLoader.execute(imageRequest)
             val bitmap = result.drawable?.toBitmap()
             return bitmap?.let { IconCompat.createWithBitmap(it) }
         } catch (e: Throwable) {
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
index 15c236eac9b..4abdc031238 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
@@ -17,6 +17,7 @@
 package io.element.android.libraries.push.impl.notifications

 import android.app.Notification
+import coil.ImageLoader
 import io.element.android.libraries.matrix.api.core.RoomId
 import io.element.android.libraries.matrix.api.user.MatrixUser
 import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
@@ -36,6 +37,7 @@ class NotificationFactory @Inject constructor(

     suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
         currentUser: MatrixUser,
+        imageLoader: ImageLoader,
     ): List<RoomNotification> {
         return map { (roomId, events) ->
             when {
@@ -46,6 +48,7 @@ class NotificationFactory @Inject constructor(
                         currentUser = currentUser,
                         events = messageEvents,
                         roomId = roomId,
+                        imageLoader = imageLoader,
                     )
                 }
             }
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
index 713392c6958..d05f3a1d03e 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
@@ -16,6 +16,7 @@

 package io.element.android.libraries.push.impl.notifications

+import coil.ImageLoader
 import io.element.android.libraries.core.log.logger.LoggerTag
 import io.element.android.libraries.matrix.api.core.RoomId
 import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -38,11 +39,12 @@ class NotificationRenderer @Inject constructor(
     suspend fun render(
         currentUser: MatrixUser,
         useCompleteNotificationFormat: Boolean,
-        eventsToProcess: List<ProcessedEvent<NotifiableEvent>>
+        eventsToProcess: List<ProcessedEvent<NotifiableEvent>>,
+        imageLoader: ImageLoader,
     ) {
         val groupedEvents = eventsToProcess.groupByType()
         with(notificationFactory) {
-            val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser)
+            val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser, imageLoader)
             val invitationNotifications = groupedEvents.invitationEvents.toNotifications()
             val simpleNotifications = groupedEvents.simpleEvents.toNotifications()
             val fallbackNotifications = groupedEvents.fallbackEvents.toNotifications()
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
index cde4f489a21..015b32c1d09 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
@@ -23,6 +23,7 @@ import androidx.core.app.NotificationCompat
 import androidx.core.app.Person
 import androidx.core.text.buildSpannedString
 import androidx.core.text.inSpans
+import coil.ImageLoader
 import io.element.android.libraries.matrix.api.core.RoomId
 import io.element.android.libraries.matrix.api.user.MatrixUser
 import io.element.android.libraries.push.impl.R
@@ -42,6 +43,7 @@ class RoomGroupMessageCreator @Inject constructor(
         currentUser: MatrixUser,
         events: List<NotifiableMessageEvent>,
         roomId: RoomId,
+        imageLoader: ImageLoader,
     ): RoomNotification.Message {
         val lastKnownRoomEvent = events.last()
         val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
@@ -49,13 +51,13 @@ class RoomGroupMessageCreator @Inject constructor(
         val style = NotificationCompat.MessagingStyle(
             Person.Builder()
                 .setName(currentUser.displayName?.annotateForDebug(50))
-                .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl))
+                .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl, imageLoader))
                 .setKey(lastKnownRoomEvent.sessionId.value)
                 .build()
         ).also {
             it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
             it.isGroupConversation = roomIsGroup
-            it.addMessagesFromEvents(events)
+            it.addMessagesFromEvents(events, imageLoader)
         }

         val tickerText = if (roomIsGroup) {
@@ -64,7 +66,7 @@ class RoomGroupMessageCreator @Inject constructor(
             stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
         }

-        val largeBitmap = getRoomBitmap(events)
+        val largeBitmap = getRoomBitmap(events, imageLoader)

         val lastMessageTimestamp = events.last().timestamp
         val smartReplyErrors = events.filter { it.isSmartReplyError() }
@@ -98,14 +100,17 @@ class RoomGroupMessageCreator @Inject constructor(
         )
     }

-    private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
+    private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(
+        events: List<NotifiableMessageEvent>,
+        imageLoader: ImageLoader,
+    ) {
         events.forEach { event ->
             val senderPerson = if (event.outGoingMessage) {
                 null
             } else {
                 Person.Builder()
                     .setName(event.senderName?.annotateForDebug(70))
-                    .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
+                    .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
                     .setKey(event.senderId.value)
                     .build()
             }
@@ -167,10 +172,13 @@ class RoomGroupMessageCreator @Inject constructor(
         }
     }

-    private suspend fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
+    private suspend fun getRoomBitmap(
+        events: List<NotifiableMessageEvent>,
+        imageLoader: ImageLoader,
+    ): Bitmap? {
         // Use the last event (most recent?)
         return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath }
-            ?.let { bitmapLoader.getRoomBitmap(it) }
+            ?.let { bitmapLoader.getRoomBitmap(it, imageLoader) }
     }
 }

diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt
index 900d9ccc698..948cc603c33 100644
--- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt
+++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/FakeComposablePermissionStateProvider.kt
@@ -27,7 +27,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
 import com.google.accompanist.permissions.PermissionState
 import com.google.accompanist.permissions.PermissionStatus

-class FakeComposablePermissionStateProvider constructor(
+class FakeComposablePermissionStateProvider(
     private val permissionState: FakePermissionState
 ) : ComposablePermissionStateProvider {
     private lateinit var onPermissionResult: (Boolean) -> Unit
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
index 6b4ea24fd46..e641029c0d0 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
 import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
 import io.element.android.libraries.matrix.test.core.aBuildMeta
 import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
+import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
 import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
 import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
 import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
@@ -129,6 +130,7 @@ class DefaultNotificationDrawerManagerTest {
             dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
             buildMeta = aBuildMeta(),
             matrixClientProvider = FakeMatrixClientProvider(),
+            imageLoaderHolder = FakeImageLoaderHolder(),
         )
     }
 }
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
index 18d8870ac38..ca41fcc15a4 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
@@ -23,6 +23,7 @@ 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
 import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
+import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
 import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
 import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
 import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
@@ -30,12 +31,15 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNoti
 import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner

 private val MY_AVATAR_URL: String? = null
 private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
 private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
 private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)

+@RunWith(RobolectricTestRunner::class)
 class NotificationFactoryTest {

     private val androidNotificationFactory = FakeAndroidNotificationFactory()
@@ -130,11 +134,14 @@ class NotificationFactoryTest {
         )
         val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))

+        val fakeImageLoader = FakeImageLoader()
         val result = roomWithMessage.toNotifications(
-            MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+            currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
+            imageLoader = fakeImageLoader.getImageLoader(),
         )

         assertThat(result).isEqualTo(listOf(expectedNotification))
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
@@ -142,8 +149,10 @@ class NotificationFactoryTest {
         val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
         val emptyRoom = mapOf(A_ROOM_ID to events)

+        val fakeImageLoader = FakeImageLoader()
         val result = emptyRoom.toNotifications(
-            MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+            currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
+            imageLoader = fakeImageLoader.getImageLoader(),
         )

         assertThat(result).isEqualTo(
@@ -153,14 +162,17 @@ class NotificationFactoryTest {
                 )
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
     fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
         val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))

+        val fakeImageLoader = FakeImageLoader()
         val result = redactedRoom.toNotifications(
-            MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+            currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
+            imageLoader = fakeImageLoader.getImageLoader(),
         )

         assertThat(result).isEqualTo(
@@ -170,6 +182,7 @@ class NotificationFactoryTest {
                 )
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
@@ -189,11 +202,14 @@ class NotificationFactoryTest {
             A_ROOM_ID,
         )

+        val fakeImageLoader = FakeImageLoader()
         val result = roomWithRedactedMessage.toNotifications(
-            MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+            currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
+            imageLoader = fakeImageLoader.getImageLoader(),
         )

         assertThat(result).isEqualTo(listOf(expectedNotification))
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }
 }

diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
index 80875406c7e..18f2266b873 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
@@ -21,12 +21,15 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
 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
+import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
 import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
 import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
 import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
 import io.mockk.mockk
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner

 private const val MY_USER_DISPLAY_NAME = "display-name"
 private const val MY_USER_AVATAR_URL = "avatar-url"
@@ -42,6 +45,7 @@ private val MESSAGE_META = RoomNotification.Message.Meta(
 )
 private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)

+@RunWith(RobolectricTestRunner::class)
 class NotificationRendererTest {

     private val notificationDisplayer = FakeNotificationDisplayer()
@@ -197,7 +201,8 @@ class NotificationRendererTest {
         notificationRenderer.render(
             MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
             useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
-            eventsToProcess = AN_EVENT_LIST
+            eventsToProcess = AN_EVENT_LIST,
+            imageLoader = FakeImageLoader().getImageLoader(),
         )
     }

diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt
index 46c41454d34..399b0fc4b36 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt
@@ -17,19 +17,15 @@
 package io.element.android.libraries.push.impl.notifications

 import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
 import android.os.Build
-import coil.Coil
-import coil.ImageLoader
 import coil.annotation.ExperimentalCoilApi
-import coil.test.FakeImageLoaderEngine
 import com.google.common.truth.Truth.assertThat
 import io.element.android.libraries.matrix.api.media.MediaSource
 import io.element.android.libraries.matrix.test.A_ROOM_ID
 import io.element.android.libraries.matrix.ui.components.aMatrixUser
 import io.element.android.libraries.matrix.ui.media.MediaRequestData
 import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
+import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
 import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
 import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
 import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
@@ -50,6 +46,7 @@ class RoomGroupMessageCreatorTest {
     @Test
     fun `test createRoomMessage with one Event`() = runTest {
         val sut = createRoomGroupMessageCreator()
+        val fakeImageLoader = FakeImageLoader()
         val result = sut.createRoomMessage(
             currentUser = aMatrixUser(),
             events = listOf(
@@ -58,6 +55,7 @@ class RoomGroupMessageCreatorTest {
                 )
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -71,11 +69,13 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = false,
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
     fun `test createRoomMessage with one noisy Event`() = runTest {
         val sut = createRoomGroupMessageCreator()
+        val fakeImageLoader = FakeImageLoader()
         val result = sut.createRoomMessage(
             currentUser = aMatrixUser(),
             events = listOf(
@@ -84,6 +84,7 @@ class RoomGroupMessageCreatorTest {
                 )
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -97,6 +98,7 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = true,
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
@@ -141,20 +143,7 @@ class RoomGroupMessageCreatorTest {
         api: Int,
         expectedCoilRequests: List<Any>,
     ) = runTest {
-        val coilRequests = mutableListOf<Any>()
-        val engine = FakeImageLoaderEngine.Builder()
-            .intercept(
-                predicate = {
-                    coilRequests.add(it)
-                    true
-                },
-                drawable = ColorDrawable(Color.BLUE)
-            )
-            .build()
-        val imageLoader = ImageLoader.Builder(RuntimeEnvironment.getApplication())
-            .components { add(engine) }
-            .build()
-        Coil.setImageLoader(imageLoader)
+        val fakeImageLoader = FakeImageLoader()
         val sut = createRoomGroupMessageCreator(
             sdkIntProvider = FakeBuildVersionSdkIntProvider(api)
         )
@@ -170,6 +159,7 @@ class RoomGroupMessageCreatorTest {
                 )
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -183,12 +173,13 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = false,
             )
         )
-        assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests)
+        assertThat(fakeImageLoader.getCoilRequests()).isEqualTo(expectedCoilRequests)
     }

     @Test
     fun `test createRoomMessage with two Events`() = runTest {
         val sut = createRoomGroupMessageCreator()
+        val fakeImageLoader = FakeImageLoader()
         val result = sut.createRoomMessage(
             currentUser = aMatrixUser(),
             events = listOf(
@@ -196,6 +187,7 @@ class RoomGroupMessageCreatorTest {
                 aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10),
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -209,11 +201,13 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = false,
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
     fun `test createRoomMessage with smart reply error`() = runTest {
         val sut = createRoomGroupMessageCreator()
+        val fakeImageLoader = FakeImageLoader()
         val result = sut.createRoomMessage(
             currentUser = aMatrixUser(),
             events = listOf(
@@ -223,6 +217,7 @@ class RoomGroupMessageCreatorTest {
                 ),
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -236,11 +231,13 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = false,
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }

     @Test
     fun `test createRoomMessage for direct room`() = runTest {
         val sut = createRoomGroupMessageCreator()
+        val fakeImageLoader = FakeImageLoader()
         val result = sut.createRoomMessage(
             currentUser = aMatrixUser(),
             events = listOf(
@@ -249,6 +246,7 @@ class RoomGroupMessageCreatorTest {
                 ),
             ),
             roomId = A_ROOM_ID,
+            imageLoader = fakeImageLoader.getImageLoader(),
         )
         val resultMetaWithoutFormatting = result.meta.copy(
             summaryLine = result.meta.summaryLine.toString()
@@ -262,6 +260,7 @@ class RoomGroupMessageCreatorTest {
                 shouldBing = false,
             )
         )
+        assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0)
     }
 }

diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt
new file mode 100644
index 00000000000..5c747b45e22
--- /dev/null
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.impl.notifications.fake
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import coil.ImageLoader
+import coil.annotation.ExperimentalCoilApi
+import coil.test.FakeImageLoaderEngine
+import org.robolectric.RuntimeEnvironment
+
+@OptIn(ExperimentalCoilApi::class)
+class FakeImageLoader {
+    private val coilRequests = mutableListOf<Any>()
+
+    private var cache: ImageLoader? = null
+
+    fun getImageLoader(): ImageLoader {
+        return cache ?: ImageLoader.Builder(RuntimeEnvironment.getApplication())
+            .components {
+                val engine = FakeImageLoaderEngine.Builder()
+                    .intercept(
+                        predicate = {
+                            coilRequests.add(it)
+                            true
+                        },
+                        drawable = ColorDrawable(Color.BLUE)
+                    )
+                    .build()
+                add(engine)
+            }
+            .build()
+            .also {
+                cache = it
+            }
+    }
+
+    fun getCoilRequests(): List<Any> {
+        return coilRequests.toList()
+    }
+}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt
similarity index 58%
rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt
rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt
index 919971b3070..0e18036e943 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixUIBindings.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt
@@ -14,13 +14,15 @@
  * limitations under the License.
  */

-package io.element.android.libraries.matrix.ui.di
+package io.element.android.libraries.push.impl.notifications.fake

-import com.squareup.anvil.annotations.ContributesTo
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.matrix.ui.media.LoggedInImageLoaderFactory
+import coil.ImageLoader
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder

-@ContributesTo(SessionScope::class)
-interface MatrixUIBindings {
-    fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory
+class FakeImageLoaderHolder : ImageLoaderHolder {
+    private val fakeImageLoader = FakeImageLoader()
+    override fun get(client: MatrixClient): ImageLoader {
+        return fakeImageLoader.getImageLoader()
+    }
 }
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
index 60b9e10c3dd..9c7755aa9d0 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
@@ -40,7 +40,7 @@ class FakeNotificationFactory {
         summaryNotification: SummaryNotification
     ) {
         with(instance) {
-            coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
+            coEvery { groupedEvents.roomEvents.toNotifications(matrixUser, any()) } returns roomNotifications
             every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
             every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
             every { groupedEvents.fallbackEvents.toNotifications() } returns fallbackNotifications
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
index b896737e6fd..946b0ab0db8 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
@@ -39,6 +39,7 @@ class FakeRoomGroupMessageCreator {
                 currentUser = matrixUser,
                 events = events,
                 roomId = roomId,
+                imageLoader = any(),
             )
         } returns mockMessage
         return mockMessage


Add special notifications for mentions

<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [x] Feature
- [ ] Bugfix
- [ ] Technical
- [ ] Other :

## Content

- Sets a special type of notification message for received messages mentioning the current user.

## Motivation and context

Closes #1451

## Screenshots / GIFs

|Before|After|
|-|-|
||<img width="484" alt="image" src="https://github.com/vector-im/element-x-android/assets/480955/aa1a98b5-5c3d-439b-85c6-e979883a7598">|

## Tests

<!-- Explain how you tested your development -->

- Receive a message mentioning you in a room you have notifications enabled.

## Tested devices

- [x] Physical
- [ ] Emulator
- OS version(s): 11

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [x] Pull request is based on the develop branch
- [x] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [x] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [x] You've made a self review of your PR

Mentions: Push notifications display user mentions differently
From element-x-ios created by [Velin92](https://github.com/Velin92): vector-im/element-x-ios#1825

Mentions should be displayed with @username in the notification (however since the username is usually not available in the notification context, is also okay to prefix with @ the hyperlink text just to make it clear that is a mention from the notification)

Also if you have been mentioned in the notification we should prefix the notification body with:
"Username mentioned you"
or
"You have been mentioned" if the username of the sender is not available

diff --git a/changelog.d/1451.feature b/changelog.d/1451.feature
new file mode 100644
index 00000000000..1869f0ecd50
--- /dev/null
+++ b/changelog.d/1451.feature
@@ -0,0 +1,1 @@
+Display different notifications for mentions.
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index 9093b366151..26c8a7d55b6 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -82,6 +82,11 @@ class NotifiableEventResolver @Inject constructor(
         return when (val content = this.content) {
             is NotificationContent.MessageLike.RoomMessage -> {
                 val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value)
+                val notificationBody = if (hasMention) {
+                    stringProvider.getString(R.string.notification_mentioned_you_body, messageBody)
+                } else {
+                    messageBody
+                }
                 buildNotifiableMessageEvent(
                     sessionId = userId,
                     senderId = content.senderId,
@@ -90,7 +95,7 @@ class NotifiableEventResolver @Inject constructor(
                     noisy = isNoisy,
                     timestamp = this.timestamp,
                     senderName = senderDisplayName,
-                    body = messageBody,
+                    body = notificationBody,
                     imageUriString = this.contentUrl,
                     roomName = roomDisplayName,
                     roomIsDirect = isDirect,
diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml
index 84525a427ef..2b76d7b93e4 100644
--- a/libraries/push/impl/src/main/res/values-cs/translations.xml
+++ b/libraries/push/impl/src/main/res/values-cs/translations.xml
@@ -8,6 +8,7 @@
   <string name="notification_invitation_action_join">"Vstoupit"</string>
   <string name="notification_invitation_action_reject">"Odmítnout"</string>
   <string name="notification_invite_body">"Vás pozval(a) do chatu"</string>
+  <string name="notification_mentioned_you_body">"Zmínili vás: %1$s"</string>
   <string name="notification_new_messages">"Nové zprávy"</string>
   <string name="notification_reaction_body">"Reagoval(a) s %1$s"</string>
   <string name="notification_room_action_mark_as_read">"Označit jako přečtené"</string>
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index fae900dd9c3..c816850f98b 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -227,6 +227,9 @@
     <item quantity="other">"%d votes"</item>
   </plurals>
   <string name="preference_rageshake">"Rageshake to report bug"</string>
+  <string name="screen_edit_poll_delete_confirmation">"Are you sure you want to delete this poll?"</string>
+  <string name="screen_edit_poll_delete_confirmation_title">"Delete Poll"</string>
+  <string name="screen_edit_poll_title">"Edit poll"</string>
   <string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
   <string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
   <string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>

diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt
index 819a1e94a23..6276983cc9d 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt
@@ -25,8 +25,10 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
 import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
 import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
 import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
 import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
@@ -51,6 +53,7 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config

 @RunWith(RobolectricTestRunner::class)
 class NotifiableEventResolverTest {
@@ -97,6 +100,71 @@ class NotifiableEventResolverTest {
         assertThat(result).isEqualTo(expectedResult)
     }

+    @Test
+    @Config(qualifiers = "en")
+    fun `resolve event message with mention`() = runTest {
+        val sut = createNotifiableEventResolver(
+            notificationResult = Result.success(
+                createNotificationData(
+                    content = NotificationContent.MessageLike.RoomMessage(
+                        senderId = A_USER_ID_2,
+                        messageType = TextMessageType(body = "Hello world", formatted = null)
+                    ),
+                    hasMention = true,
+                )
+            )
+        )
+        val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+        val expectedResult = createNotifiableMessageEvent(body = "Mentioned you: Hello world")
+        assertThat(result).isEqualTo(expectedResult)
+    }
+
+    @Test
+    fun `resolve HTML formatted event message text takes plain text version`() = runTest {
+        val sut = createNotifiableEventResolver(
+            notificationResult = Result.success(
+                createNotificationData(
+                    content = NotificationContent.MessageLike.RoomMessage(
+                        senderId = A_USER_ID_2,
+                        messageType = TextMessageType(
+                            body = "Hello world!",
+                            formatted = FormattedBody(
+                                body = "<b>Hello world</b>",
+                                format = MessageFormat.HTML,
+                            )
+                        )
+                    )
+                )
+            )
+        )
+        val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+        val expectedResult = createNotifiableMessageEvent(body = "Hello world")
+        assertThat(result).isEqualTo(expectedResult)
+    }
+
+    @Test
+    fun `resolve incorrectly formatted event message text uses fallback`() = runTest {
+        val sut = createNotifiableEventResolver(
+            notificationResult = Result.success(
+                createNotificationData(
+                    content = NotificationContent.MessageLike.RoomMessage(
+                        senderId = A_USER_ID_2,
+                        messageType = TextMessageType(
+                            body = "Hello world",
+                            formatted = FormattedBody(
+                                body = "???Hello world!???",
+                                format = MessageFormat.UNKNOWN,
+                            )
+                        )
+                    )
+                )
+            )
+        )
+        val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+        val expectedResult = createNotifiableMessageEvent(body = "Hello world")
+        assertThat(result).isEqualTo(expectedResult)
+    }
+
     @Test
     fun `resolve event message audio`() = runTest {
         val sut = createNotifiableEventResolver(
@@ -430,6 +498,7 @@ class NotifiableEventResolverTest {
     private fun createNotificationData(
         content: NotificationContent,
         isDirect: Boolean = false,
+        hasMention: Boolean = false,
     ): NotificationData {
         return NotificationData(
             eventId = AN_EVENT_ID,
@@ -444,7 +513,7 @@ class NotifiableEventResolverTest {
             timestamp = A_TIMESTAMP,
             content = content,
             contentUrl = null,
-            hasMention = false,
+            hasMention = hasMention,
         )
     }