diff --git a/briar b/briar
index 20b52804bf4a4306739eeafa4de9f312ffdea9b9..65be2d2b2655062c539d6b4ba931bfe730ece2e2 160000
--- a/briar
+++ b/briar
@@ -1 +1 @@
-Subproject commit 20b52804bf4a4306739eeafa4de9f312ffdea9b9
+Subproject commit 65be2d2b2655062c539d6b4ba931bfe730ece2e2
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index c5756288c17207ed5da5754fe5426d11b7cdbec7..e5727398c46921001bd19449aac5c2f9ad5b4c56 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -30,12 +30,16 @@ import org.briarproject.bramble.system.DesktopSecureRandomModule
 import org.briarproject.bramble.system.JavaSystemModule
 import org.briarproject.bramble.util.OsUtils.isLinux
 import org.briarproject.bramble.util.OsUtils.isMac
+import org.briarproject.briar.attachment.AttachmentModule
+import org.briarproject.briar.desktop.attachment.media.ImageCompressor
+import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
+import org.briarproject.briar.identity.IdentityModule
 import java.io.File
 import java.nio.file.Path
 import java.util.Collections.emptyList
@@ -45,6 +49,7 @@ import javax.inject.Singleton
 @Module(
     includes = [
         AccountModule::class,
+        IdentityModule::class,
         CircumventionModule::class,
         ClockModule::class,
         DefaultBatteryManagerModule::class,
@@ -55,6 +60,7 @@ import javax.inject.Singleton
         JavaSystemModule::class,
         SocksModule::class,
         ViewModelModule::class,
+        AttachmentModule::class,
     ]
 )
 internal class DesktopModule(
@@ -123,4 +129,10 @@ internal class DesktopModule(
         override fun shouldEnableProfilePictures() = false
         override fun shouldEnableDisappearingMessages() = false
     }
+
+    @Provides
+    @Singleton
+    internal fun provideImageCompressor(imageCompressor: ImageCompressorImpl): ImageCompressor {
+        return imageCompressor
+    }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressor.kt b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a9115a53da5d8b6ea5d44af0f15135d065d26d44
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressor.kt
@@ -0,0 +1,19 @@
+package org.briarproject.briar.desktop.attachment.media
+
+import java.awt.image.BufferedImage
+import java.io.IOException
+import java.io.InputStream
+
+interface ImageCompressor {
+
+    /**
+     * Compress an image and return an InputStream from which the resulting
+     * image can be read. The image will be compressed as a JPEG image such that
+     * it fits into a message.
+     *
+     * @param image the source image
+     * @return a stream from which the resulting image can be read
+     */
+    @Throws(IOException::class)
+    fun compressImage(image: BufferedImage): InputStream
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c3b515652d6d1cf27d3b402bea65efc0748551be
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/attachment/media/ImageCompressorImpl.kt
@@ -0,0 +1,43 @@
+package org.briarproject.briar.desktop.attachment.media
+
+import mu.KotlinLogging
+import org.briarproject.briar.api.attachment.MediaConstants.MAX_IMAGE_SIZE
+import java.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import javax.imageio.IIOImage
+import javax.imageio.ImageIO
+import javax.imageio.ImageWriteParam
+import javax.inject.Inject
+
+class ImageCompressorImpl @Inject internal constructor() : ImageCompressor {
+
+    companion object {
+        val LOG = KotlinLogging.logger {}
+    }
+
+    override fun compressImage(image: BufferedImage): InputStream {
+        val out = ByteArrayOutputStream()
+        for (quality in 100 downTo 1 step 10) {
+            val jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next()
+            jpgWriter.output = ImageIO.createImageOutputStream(out)
+
+            val jpgWriteParam = jpgWriter.defaultWriteParam
+            jpgWriteParam.compressionMode = ImageWriteParam.MODE_EXPLICIT
+            jpgWriteParam.compressionQuality = quality / 100f
+
+            val outputImage = IIOImage(image, null, null)
+            jpgWriter.write(null, outputImage, jpgWriteParam)
+
+            jpgWriter.dispose()
+            if (out.size() <= MAX_IMAGE_SIZE) {
+                LOG.info { "Compressed image to ${out.size()} bytes, quality $quality" }
+                return ByteArrayInputStream(out.toByteArray())
+            }
+            out.reset()
+        }
+        throw IOException()
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index 6147e41fe80af7fe7b06a1ad560d51e3c470be75..8b6744ddd4ff5e5e21e83852f9e5d1edcbba9d39 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -56,7 +56,8 @@ fun main() = preview(
             isConnected = getBooleanParameter("isConnected"),
             isEmpty = getBooleanParameter("isEmpty"),
             unread = getIntParameter("unread"),
-            timestamp = getLongParameter("timestamp")
+            timestamp = getLongParameter("timestamp"),
+            avatar = null,
         ),
         {}, getBooleanParameter("selected")
     )
@@ -84,8 +85,7 @@ fun ContactCard(
                 when (contactItem) {
                     is ContactItem -> {
                         Box(modifier = Modifier.align(Alignment.CenterVertically)) {
-                            // TODO Pull profile pictures
-                            ProfileCircle(36.dp, contactItem.authorId.bytes)
+                            ProfileCircle(36.dp, contactItem)
                             MessageCounter(
                                 unread = contactItem.unread,
                                 modifier = Modifier.align(Alignment.TopEnd)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
index f9a914ec779f990689372fdb824a63ddd4936b98..b222a55776cf0b4e77f79d823dd9a233b1c7328b 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
@@ -1,5 +1,6 @@
 package org.briarproject.briar.desktop.contact
 
+import androidx.compose.ui.graphics.ImageBitmap
 import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.bramble.api.identity.AuthorId
 import org.briarproject.briar.api.client.MessageTracker
@@ -14,12 +15,18 @@ data class ContactItem(
     val isConnected: Boolean,
     val isEmpty: Boolean,
     val unread: Int,
-    override val timestamp: Long
+    override val timestamp: Long,
+    val avatar: ImageBitmap?,
 ) : BaseContactItem {
 
     override val displayName = getContactDisplayName(name, alias)
 
-    constructor(contact: Contact, isConnected: Boolean, groupCount: MessageTracker.GroupCount) : this(
+    constructor(
+        contact: Contact,
+        isConnected: Boolean,
+        groupCount: MessageTracker.GroupCount,
+        avatar: ImageBitmap?
+    ) : this(
         idWrapper = RealContactIdWrapper(contact.id),
         authorId = contact.author.id,
         name = contact.author.name,
@@ -27,7 +34,8 @@ data class ContactItem(
         isConnected = isConnected,
         isEmpty = groupCount.msgCount == 0,
         unread = groupCount.unreadCount,
-        timestamp = groupCount.latestMsgTime
+        timestamp = groupCount.latestMsgTime,
+        avatar = avatar,
     )
 
     fun updateTimestampAndUnread(timestamp: Long, read: Boolean): ContactItem =
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
index c2b3577ae2ce563baeb206a7429cb1950130c6b4..745a2addf65f7d0b124e73ed658bbd2ab93e5ac9 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
@@ -10,8 +10,11 @@ import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.briar.api.attachment.AttachmentReader
+import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.conversation.event.ConversationMessageTrackedEvent
+import org.briarproject.briar.api.identity.AuthorManager
 import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.viewmodel.asState
@@ -21,14 +24,24 @@ class ContactListViewModel
 @Inject
 constructor(
     contactManager: ContactManager,
+    authorManager: AuthorManager,
     conversationManager: ConversationManager,
     connectionRegistry: ConnectionRegistry,
+    attachmentReader: AttachmentReader,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
 ) : ContactsViewModel(
-    contactManager, conversationManager, connectionRegistry, briarExecutors, lifecycleManager, db, eventBus
+    contactManager,
+    authorManager,
+    conversationManager,
+    connectionRegistry,
+    attachmentReader,
+    briarExecutors,
+    lifecycleManager,
+    db,
+    eventBus,
 ) {
 
     companion object {
@@ -75,7 +88,6 @@ constructor(
                 LOG.info { "Conversation message tracked, updating item" }
                 updateItem(e.contactId) { it.updateTimestampAndUnread(e.timestamp, e.read) }
             }
-            // is AvatarUpdatedEvent -> {}
             is ContactAliasChangedEvent -> {
                 updateItem(e.contactId) { it.updateAlias(e.alias) }
             }
@@ -83,6 +95,10 @@ constructor(
                 LOG.info("${e.count} conversation messages read, updating item")
                 updateItem(e.contactId) { it.updateFromMessagesRead(e.count) }
             }
+            is AvatarUpdatedEvent -> {
+                LOG.info("received avatar update: ${e.attachmentHeader}")
+                // TODO: update avatar
+            }
         }
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
index 0fcf16cd13ae79d49ccc032728c128604c0e39ee..848ab77a18f5549f5d0da2c80c3325ac0770febf 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -15,8 +15,11 @@ import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
 import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
+import org.briarproject.briar.api.attachment.AttachmentReader
 import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.identity.AuthorManager
 import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.utils.ImageUtils.loadAvatar
 import org.briarproject.briar.desktop.utils.clearAndAddAll
 import org.briarproject.briar.desktop.utils.removeFirst
 import org.briarproject.briar.desktop.utils.replaceFirst
@@ -24,8 +27,10 @@ import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 
 abstract class ContactsViewModel(
     protected val contactManager: ContactManager,
+    private val authorManager: AuthorManager,
     private val conversationManager: ConversationManager,
     private val connectionRegistry: ConnectionRegistry,
+    private val attachmentReader: AttachmentReader,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
@@ -58,6 +63,7 @@ abstract class ContactsViewModel(
                         contact,
                         connectionRegistry.isConnected(contact.id),
                         conversationManager.getGroupCount(txn, contact.id),
+                        loadAvatar(authorManager, attachmentReader, txn, contact),
                     )
                 }
             )
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
index 34e723dafe870b63a8f552016aedfd6ec8052723..3c53a2ce1aa54f98d482c99fc19065bc3fe25a9d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
@@ -2,6 +2,7 @@ package org.briarproject.briar.desktop.contact
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
@@ -9,6 +10,8 @@ import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.theme.outline
@@ -24,8 +27,25 @@ fun PreviewProfileCircle() {
     ProfileCircle(90.dp, bytes)
 }
 
+/**
+ * Display the avatar for a [ContactItem]. If it has an avatar image, display that, otherwise
+ * display an [Identicon] based on the user's author id. Either way the profile image is displayed
+ * within a circle.
+ *
+ * @param size the size of the circle. In order to avoid aliasing effects for Identicon-based profile images,
+ *             pass a multiple of 9 here. That helps as the image is based on a 9x9 square grid.
+ */
+@Composable
+fun ProfileCircle(size: Dp, contactItem: ContactItem) {
+    if (contactItem.avatar == null)
+        ProfileCircle(size, contactItem.authorId.bytes)
+    else
+        ProfileCircle(size, contactItem.avatar)
+}
+
 /**
  * Display an [Identicon] as a profile image within a circle based on a user's author id.
+ *
  * @param size the size of the circle. In order to avoid aliasing effects, pass a multiple
  *             of 9 here. That helps as the image is based on a 9x9 square grid.
  */
@@ -37,7 +57,24 @@ fun ProfileCircle(size: Dp, input: ByteArray) {
 }
 
 /**
- * Used for pending contacts.
+ * Display an avatar bitmap as a profile image within a circle.
+ *
+ * @param size the size of the circle.
+ */
+@Composable
+fun ProfileCircle(size: Dp, avatar: ImageBitmap) {
+    Image(
+        bitmap = avatar,
+        contentDescription = null,
+        contentScale = ContentScale.FillBounds,
+        modifier = Modifier.size(size).clip(CircleShape).border(2.dp, MaterialTheme.colors.outline, CircleShape),
+    )
+}
+
+/**
+ * Display a placeholder avatar for pending contacts.
+ *
+ * @param size the size of the circle.
  */
 @Composable
 fun ProfileCircle(size: Dp) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
index deeca393355e666042958afdb4135950c970ba6a..a6566260d67adf60248d6230b267d161bcf486fc 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
@@ -42,7 +42,7 @@ fun ConversationHeader(
 
     Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) {
         Row(modifier = Modifier.align(Alignment.Center)) {
-            ProfileCircle(36.dp, contactItem.authorId.bytes)
+            ProfileCircle(36.dp, contactItem)
             Canvas(
                 modifier = Modifier.align(Alignment.CenterVertically),
                 onDraw = {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index adbbb13391584c567733d90c3d8096b7087ccd3f..bef737f175fa25ec663d0e77151b710072d8848d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -24,10 +24,12 @@ import org.briarproject.bramble.api.sync.event.MessagesAckedEvent
 import org.briarproject.bramble.api.sync.event.MessagesSentEvent
 import org.briarproject.bramble.api.versioning.event.ClientVersionUpdatedEvent
 import org.briarproject.bramble.util.LogUtils
+import org.briarproject.briar.api.attachment.AttachmentReader
 import org.briarproject.briar.api.autodelete.UnexpectedTimerException
 import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
+import org.briarproject.briar.api.identity.AuthorManager
 import org.briarproject.briar.api.introduction.IntroductionManager
 import org.briarproject.briar.api.messaging.MessagingManager
 import org.briarproject.briar.api.messaging.PrivateMessage
@@ -36,6 +38,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader
 import org.briarproject.briar.desktop.contact.ContactItem
 import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.INTRODUCTION
 import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.utils.ImageUtils.loadAvatar
 import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration
 import org.briarproject.briar.desktop.utils.clearAndAddAll
 import org.briarproject.briar.desktop.utils.replaceIf
@@ -50,6 +53,7 @@ class ConversationViewModel
 constructor(
     private val connectionRegistry: ConnectionRegistry,
     private val contactManager: ContactManager,
+    private val authorManager: AuthorManager,
     private val conversationManager: ConversationManager,
     private val introductionManager: IntroductionManager,
     private val messagingManager: MessagingManager,
@@ -57,6 +61,7 @@ constructor(
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
+    private val attachmentReader: AttachmentReader,
     private val eventBus: EventBus,
 ) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
 
@@ -167,10 +172,14 @@ constructor(
     private fun loadContact(txn: Transaction, id: ContactId): ContactItem {
         try {
             val start = LogUtils.now()
+
+            val contact = contactManager.getContact(txn, id)
+
             val contactItem = ContactItem(
-                contactManager.getContact(txn, id),
+                contact,
                 connectionRegistry.isConnected(id),
                 conversationManager.getGroupCount(txn, id),
+                loadAvatar(authorManager, attachmentReader, txn, contact),
             )
             LOG.logDuration("Loading contact", start)
             txn.attach { _contactItem.value = contactItem }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
index 7ee9d42f34a535911e2b8c9112e6d1933f2d0d1b..c49ea03c788370cbf07346ed1a7c532ef6db3421 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.contact.ContactManager
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.briar.api.attachment.AttachmentReader
 import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.identity.AuthorManager
 import org.briarproject.briar.api.introduction.IntroductionManager
 import org.briarproject.briar.desktop.contact.BaseContactItem
 import org.briarproject.briar.desktop.contact.ContactItem
@@ -20,14 +22,24 @@ class IntroductionViewModel
 constructor(
     private val introductionManager: IntroductionManager,
     contactManager: ContactManager,
+    authorManager: AuthorManager,
     conversationManager: ConversationManager,
     connectionRegistry: ConnectionRegistry,
+    attachmentReader: AttachmentReader,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
 ) : ContactsViewModel(
-    contactManager, conversationManager, connectionRegistry, briarExecutors, lifecycleManager, db, eventBus
+    contactManager,
+    authorManager,
+    conversationManager,
+    connectionRegistry,
+    attachmentReader,
+    briarExecutors,
+    lifecycleManager,
+    db,
+    eventBus,
 ) {
 
     private val _firstContact = mutableStateOf<ContactItem?>(null)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/ImageUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/ImageUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8a346ddaed28001629c5588ee76217df13fd5a1
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/ImageUtils.kt
@@ -0,0 +1,27 @@
+package org.briarproject.briar.desktop.utils
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.res.loadImageBitmap
+import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.bramble.api.db.Transaction
+import org.briarproject.briar.api.attachment.AttachmentReader
+import org.briarproject.briar.api.identity.AuthorManager
+
+object ImageUtils {
+
+    fun loadAvatar(
+        authorManager: AuthorManager,
+        attachmentReader: AttachmentReader,
+        txn: Transaction,
+        contact: Contact
+    ): ImageBitmap? {
+        val authorInfo = authorManager.getAuthorInfo(txn, contact)
+        if (authorInfo.avatarHeader == null) {
+            return null
+        }
+        val attachment = attachmentReader.getAttachment(txn, authorInfo.avatarHeader)
+        attachment.stream.use {
+            return loadImageBitmap(it)
+        }
+    }
+}
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index 101340c5276c0b27a7d32e3296b8cb8a88238209..7bf547826efa6f7d2245ca5cddd34557370cd223 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -31,14 +31,19 @@ import org.briarproject.bramble.system.JavaSystemModule
 import org.briarproject.bramble.util.OsUtils.isLinux
 import org.briarproject.bramble.util.OsUtils.isMac
 import org.briarproject.briar.api.test.TestAvatarCreator
+import org.briarproject.briar.attachment.AttachmentModule
+import org.briarproject.briar.desktop.attachment.media.ImageCompressor
+import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl
+import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
+import org.briarproject.briar.identity.IdentityModule
 import org.briarproject.briar.test.TestModule
 import java.io.File
 import java.nio.file.Path
@@ -49,6 +54,7 @@ import javax.inject.Singleton
 @Module(
     includes = [
         AccountModule::class,
+        IdentityModule::class,
         CircumventionModule::class,
         ClockModule::class,
         DefaultBatteryManagerModule::class,
@@ -59,7 +65,8 @@ import javax.inject.Singleton
         JavaSystemModule::class,
         SocksModule::class,
         TestModule::class,
-        ViewModelModule::class
+        ViewModelModule::class,
+        AttachmentModule::class,
     ]
 )
 internal class DesktopTestModule(
@@ -130,7 +137,16 @@ internal class DesktopTestModule(
     }
 
     @Provides
-    internal fun provideTestAvatarCreator() = TestAvatarCreator { null }
+    @Singleton
+    internal fun provideImageCompressor(imageCompressor: ImageCompressorImpl): ImageCompressor {
+        return imageCompressor
+    }
+
+    @Provides
+    @Singleton
+    internal fun provideTestAvatarCreator(testAvatarCreator: TestAvatarCreatorImpl): TestAvatarCreator {
+        return testAvatarCreator
+    }
 
     @Provides
     @Singleton
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/contact/ContactItemTest.kt b/src/test/kotlin/org/briarproject/briar/desktop/contact/ContactItemTest.kt
index 7f1ac6910312b573acc30766b36db15804053dc9..7376e74ba74abd1fa4c755ccab598c80d0571333 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/contact/ContactItemTest.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/contact/ContactItemTest.kt
@@ -1,8 +1,13 @@
 package org.briarproject.briar.desktop.contact
 
 import org.briarproject.bramble.api.UniqueId
+import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.bramble.api.crypto.CryptoConstants
+import org.briarproject.bramble.api.crypto.SignaturePublicKey
+import org.briarproject.bramble.api.identity.Author
 import org.briarproject.bramble.api.identity.AuthorId
+import org.briarproject.briar.api.client.MessageTracker
 import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -12,15 +17,27 @@ class ContactItemTest {
     @Test
     fun test() {
         val random = Random(1)
+
+        val localAuthorId = AuthorId(random.nextBytes(UniqueId.LENGTH))
+
+        val id = AuthorId(random.nextBytes(UniqueId.LENGTH))
+        val name = "Alice"
+        val publicKey = SignaturePublicKey(random.nextBytes(CryptoConstants.MAX_SIGNATURE_PUBLIC_KEY_BYTES))
+        val author = Author(id, Author.FORMAT_VERSION, name, publicKey)
+
+        val contact = Contact(
+            ContactId(random.nextInt()),
+            author,
+            localAuthorId,
+            null,
+            null,
+            false,
+        )
         val item = ContactItem(
-            idWrapper = RealContactIdWrapper(ContactId(random.nextInt())),
-            authorId = AuthorId(random.nextBytes(UniqueId.LENGTH)),
-            name = "Alice",
-            alias = null,
+            contact = contact,
             isConnected = false,
-            isEmpty = false,
-            unread = 10,
-            timestamp = random.nextLong()
+            groupCount = MessageTracker.GroupCount(0, 0, System.currentTimeMillis()),
+            avatar = null
         )
         assertEquals("Alice", item.displayName)
 
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt
index 9c336eee4d4378a5807c5969539eff434dd8926d..47a754fbf597dc7abd3ff80d43a24ff88dcc0746 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/testdata/DeterministicTestDataCreatorImpl.kt
@@ -66,7 +66,9 @@ class DeterministicTestDataCreatorImpl @Inject internal constructor(
     private val avatarMessageEncoder: AvatarMessageEncoder,
     private val clientHelper: ClientHelper,
     private val eventBus: EventBus,
-    @field:IoExecutor @param:IoExecutor private val ioExecutor: Executor
+    @field:IoExecutor
+    @param:IoExecutor
+    private val ioExecutor: Executor,
 ) : DeterministicTestDataCreator {
 
     companion object {
@@ -75,6 +77,7 @@ class DeterministicTestDataCreatorImpl @Inject internal constructor(
 
     private val random = Random()
     private val localAuthors: MutableMap<ContactId, LocalAuthor> = HashMap()
+
     override fun createTestData(
         numContacts: Int,
         numPrivateMsgs: Int,
@@ -204,6 +207,7 @@ class DeterministicTestDataCreatorImpl @Inject internal constructor(
             props[TorConstants.ID] = tor
             return props
         }
+
     private val randomBluetoothAddress: String
         get() {
             val mac = ByteArray(6)
@@ -215,6 +219,7 @@ class DeterministicTestDataCreatorImpl @Inject internal constructor(
             }
             return sb.toString()
         }
+
     private val randomUUID: String
         get() {
             val uuid = ByteArray(BluetoothConstants.UUID_BYTES)
@@ -240,6 +245,7 @@ class DeterministicTestDataCreatorImpl @Inject internal constructor(
             sb.append(':').append(randomPortNumber)
             return sb.toString()
         }
+
     private val randomPortNumber: Int
         get() = 32768 + random.nextInt(32768)
 
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/testdata/TestAvatarCreatorImpl.kt b/src/test/kotlin/org/briarproject/briar/desktop/testdata/TestAvatarCreatorImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2bf319fba25a24a89a35b1632474f6290e5b5279
--- /dev/null
+++ b/src/test/kotlin/org/briarproject/briar/desktop/testdata/TestAvatarCreatorImpl.kt
@@ -0,0 +1,74 @@
+package org.briarproject.briar.desktop.testdata
+
+import org.briarproject.briar.api.test.TestAvatarCreator
+import org.briarproject.briar.desktop.attachment.media.ImageCompressor
+import java.awt.Color
+import java.awt.Graphics2D
+import java.awt.RenderingHints
+import java.awt.image.BufferedImage
+import java.io.InputStream
+import java.lang.Integer.max
+import javax.inject.Inject
+import kotlin.random.Random
+
+class TestAvatarCreatorImpl @Inject internal constructor(private val imageCompressor: ImageCompressor) :
+    TestAvatarCreator {
+
+    private val WIDTH = 800
+    private val HEIGHT = 640
+
+    private val random = Random(0)
+
+    override fun getAvatarInputStream(): InputStream {
+        val image = BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB)
+
+        if (random.nextBoolean()) {
+            generateColoredPixels(image)
+        } else {
+            generateColoredCircles(image)
+        }
+
+        return imageCompressor.compressImage(image)
+    }
+
+    private fun generateColoredPixels(image: BufferedImage) {
+        val g: Graphics2D = image.createGraphics()
+        val pixelMultiplier: Int = random.nextInt(500) + 1
+
+        for (x in 0..WIDTH step pixelMultiplier) {
+            for (y in 0..HEIGHT step pixelMultiplier) {
+                g.color = Color(getRandomColor())
+                g.fillRect(x, y, pixelMultiplier, pixelMultiplier)
+            }
+        }
+    }
+
+    private fun generateColoredCircles(image: BufferedImage) {
+        val g: Graphics2D = image.createGraphics()
+
+        g.color = Color.WHITE
+        g.fillRect(0, 0, image.width, image.height)
+
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
+
+        val biggestSide = max(WIDTH, HEIGHT)
+        val selectedCount = random.nextInt(10) + 2
+        val radiusFrom = biggestSide / 12f
+        val radiusTo = biggestSide / 4f
+        for (i in 0..selectedCount) {
+            val cx = random.nextInt(WIDTH)
+            val cy = random.nextInt(HEIGHT)
+            val radius = (random.nextInt((radiusTo - radiusFrom).toInt()) + radiusFrom).toInt()
+            val diameter = radius * 2
+            g.color = Color(getRandomColor())
+            g.fillOval(cx - radius, cy - radius, diameter, diameter)
+        }
+    }
+
+    private fun getRandomColor(): Int {
+        val hue = random.nextFloat()
+        val saturation = random.nextFloat()
+        val brightness = 1f
+        return Color.HSBtoRGB(hue, saturation, brightness)
+    }
+}