diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
index f6f9634ca4..bf19c3cc34 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt
@@ -74,8 +74,9 @@ class AvatarSelection(
      */
     fun startAvatarSelection(
         includeClear: Boolean,
-        attemptToIncludeCamera: Boolean
-    ): File? {
+        attemptToIncludeCamera: Boolean,
+        createTempFile: ()->File?
+    ) {
         var captureFile: File? = null
         val hasCameraPermission = ContextCompat
             .checkSelfPermission(
@@ -83,18 +84,11 @@ class AvatarSelection(
                 Manifest.permission.CAMERA
             ) == PackageManager.PERMISSION_GRANTED
         if (attemptToIncludeCamera && hasCameraPermission) {
-            try {
-                captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity))
-            } catch (e: IOException) {
-                Log.e("Cannot reserve a temporary avatar capture file.", e)
-            } catch (e: NoExternalStorageException) {
-                Log.e("Cannot reserve a temporary avatar capture file.", e)
-            }
+            captureFile = createTempFile()
         }
 
         val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
         onPickImage.launch(chooserIntent)
-        return captureFile
     }
 
     private fun createAvatarSelectionIntent(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
index ebf1eacfd4..d445d002cb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
@@ -572,7 +572,7 @@ class ConversationReactionOverlay : FrameLayout {
                 items += ActionItem(R.attr.menu_save_icon,
                             R.string.save,
                             { handleActionItemClicked(Action.DOWNLOAD) },
-                            R.string.AccessibilityId_save
+                            R.string.AccessibilityId_saveAttachment
                 )
             }
         }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
index 98332b8661..a100530337 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
@@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.pager.HorizontalPager
@@ -46,6 +48,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.lifecycleScope
 import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
@@ -212,7 +215,15 @@ fun CellMetadata(
                 senderInfo?.let {
                     TitledView(state.fromTitle) {
                         Row {
-                            sender?.let { Avatar(it) }
+                            sender?.let {
+                                Avatar(
+                                    recipient = it,
+                                    modifier = Modifier
+                                        .align(Alignment.CenterVertically)
+                                        .size(46.dp)
+                                )
+                                Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing))
+                            }
                             TitledMonospaceText(it)
                         }
                     }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
index 286bc74e93..79697252bd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
@@ -34,7 +34,6 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -232,7 +231,7 @@ private fun SaveAttachmentWarningDialog(
         title = context.getString(R.string.warning),
         text = context.resources.getString(R.string.attachmentsWarning),
         buttons = listOf(
-            DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
+            DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_saveAttachment), color = LocalColors.current.danger, onClick = onAccepted),
             DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
         )
     )
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
index 75abce49a9..4abfe84f5d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
@@ -6,6 +6,7 @@ import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.graphics.BitmapFactory
 import android.net.Uri
 import android.os.Bundle
 import android.os.Parcelable
@@ -13,38 +14,52 @@ import android.util.SparseArray
 import android.view.ActionMode
 import android.view.Menu
 import android.view.MenuItem
-import android.view.View
 import android.view.inputmethod.EditorInfo
 import android.view.inputmethod.InputMethodManager
 import android.widget.Toast
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
 import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import com.canhub.cropper.CropImage
 import com.canhub.cropper.CropImageContract
 import com.squareup.phrase.Phrase
 import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
 import network.loki.messenger.BuildConfig
 import network.loki.messenger.R
 import network.loki.messenger.databinding.ActivitySettingsBinding
@@ -53,7 +68,6 @@ import nl.komponents.kovenant.ui.alwaysUi
 import nl.komponents.kovenant.ui.failUi
 import nl.komponents.kovenant.ui.successUi
 import org.session.libsession.avatars.AvatarHelper
-import org.session.libsession.avatars.ProfileContactPhoto
 import org.session.libsession.messaging.MessagingModuleConfiguration
 import org.session.libsession.snode.OnionRequestAPI
 import org.session.libsession.snode.SnodeAPI
@@ -63,34 +77,36 @@ import org.session.libsession.utilities.ProfilePictureUtilities
 import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
 import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
 import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsession.utilities.recipients.Recipient
-import org.session.libsession.utilities.truncateIdForDisplay
 import org.session.libsignal.utilities.Log
 import org.session.libsignal.utilities.Util.SECURE_RANDOM
 import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
 import org.thoughtcrime.securesms.avatar.AvatarSelection
-import org.thoughtcrime.securesms.components.ProfilePictureView
 import org.thoughtcrime.securesms.debugmenu.DebugActivity
 import org.thoughtcrime.securesms.dependencies.ConfigFactory
 import org.thoughtcrime.securesms.home.PathActivity
 import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
 import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.*
 import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
-import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
 import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
-import org.thoughtcrime.securesms.showSessionDialog
+import org.thoughtcrime.securesms.ui.AlertDialog
+import org.thoughtcrime.securesms.ui.Avatar
 import org.thoughtcrime.securesms.ui.Cell
+import org.thoughtcrime.securesms.ui.DialogButtonModel
 import org.thoughtcrime.securesms.ui.Divider
+import org.thoughtcrime.securesms.ui.GetString
 import org.thoughtcrime.securesms.ui.LargeItemButton
 import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
 import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
 import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
 import org.thoughtcrime.securesms.ui.contentDescription
 import org.thoughtcrime.securesms.ui.setThemedContent
+import org.thoughtcrime.securesms.ui.theme.LocalColors
 import org.thoughtcrime.securesms.ui.theme.LocalDimensions
+import org.thoughtcrime.securesms.ui.theme.PreviewTheme
+import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
+import org.thoughtcrime.securesms.ui.theme.ThemeColors
 import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
-import org.thoughtcrime.securesms.util.BitmapDecodingException
-import org.thoughtcrime.securesms.util.BitmapUtil
 import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
 import org.thoughtcrime.securesms.util.NetworkUtils
 import org.thoughtcrime.securesms.util.push
@@ -106,41 +122,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
     @Inject
     lateinit var prefs: TextSecurePreferences
 
+    private val viewModel: SettingsViewModel by viewModels()
+
     private lateinit var binding: ActivitySettingsBinding
     private var displayNameEditActionMode: ActionMode? = null
         set(value) { field = value; handleDisplayNameEditActionModeChanged() }
-    private var tempFile: File? = null
-
-    private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
 
     private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result ->
-        when {
-            result.isSuccessful -> {
-                Log.i(TAG, result.getUriFilePath(this).toString())
-
-                lifecycleScope.launch(Dispatchers.IO) {
-                    try {
-                        val profilePictureToBeUploaded =
-                            BitmapUtil.createScaledBytes(
-                                this@SettingsActivity,
-                                result.getUriFilePath(this@SettingsActivity).toString(),
-                                ProfileMediaConstraints()
-                            ).bitmap
-                        launch(Dispatchers.Main) {
-                            updateProfilePicture(profilePictureToBeUploaded)
-                        }
-                    } catch (e: BitmapDecodingException) {
-                        Log.e(TAG, e)
-                    }
-                }
-            }
-            result is CropImage.CancelledResult -> {
-                Log.i(TAG, "Cropping image was cancelled by the user")
-            }
-            else -> {
-                Log.e(TAG, "Cropping image failed")
-            }
-        }
+        viewModel.onAvatarPicked(result)
     }
 
     private val onPickImage = registerForActivityResult(
@@ -149,12 +138,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
         if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
 
         val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
-        val inputFile: Uri? = result.data?.data ?: tempFile?.let(Uri::fromFile)
+        val inputFile: Uri? = result.data?.data ?: viewModel.getTempFile()?.let(Uri::fromFile)
         cropImage(inputFile, outputFile)
     }
 
     private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
 
+    private var showAvatarDialog: Boolean by mutableStateOf(false)
+
     companion object {
         private const val SCROLL_STATE = "SCROLL_STATE"
     }
@@ -167,17 +158,37 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
 
         // set the toolbar icon to a close icon
         supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
+
+        // set the compose dialog content
+        binding.avatarDialog.setThemedContent {
+            if(showAvatarDialog){
+                AvatarDialogContainer(
+                    saveAvatar = {
+                        //todo TEMPORARY !!!!!!!!!!!!!!!!!!!!
+                        (viewModel.avatarDialogState.value as? TempAvatar)?.let{ updateProfilePicture(it.data) }
+                    },
+                    removeAvatar = ::removeProfilePicture,
+                    startAvatarSelection = ::startAvatarSelection
+                )
+            }
+        }
     }
 
     override fun onStart() {
         super.onStart()
 
         binding.run {
-            setupProfilePictureView(profilePictureView)
-            profilePictureView.setOnClickListener { showEditProfilePictureUI() }
+            profilePictureView.apply {
+                publicKey = viewModel.hexEncodedPublicKey
+                displayName = viewModel.getDisplayName()
+                update()
+            }
+            profilePictureView.setOnClickListener {
+                showAvatarDialog = true
+            }
             ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
-            btnGroupNameDisplay.text = getDisplayName()
-            publicKeyTextView.text = hexEncodedPublicKey
+            btnGroupNameDisplay.text = viewModel.getDisplayName()
+            publicKeyTextView.text = viewModel.hexEncodedPublicKey
             val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
             val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}"
             val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment"
@@ -195,17 +206,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
         overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_bottom)
     }
 
-    private fun getDisplayName(): String =
-        TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
-
-    private fun setupProfilePictureView(view: ProfilePictureView) {
-        view.apply {
-            publicKey = hexEncodedPublicKey
-            displayName = getDisplayName()
-            update()
-        }
-    }
-
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
         val scrollBundle = SparseArray<Parcelable>()
@@ -310,6 +310,29 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
         return updateWasSuccessful
     }
 
+//    private fun createAvatarDialog(){
+//        if (avatarDialog != null) return
+//
+//        avatarDialog = SettingsAvatarDialog(
+//            userKey = viewModel.hexEncodedPublicKey,
+//            userName = viewModel.getDisplayName(),
+//            startAvatarSelection = ::startAvatarSelection,
+//            saveAvatar = {
+//                viewModel.temporaryAvatar.value?.let{ updateProfilePicture(it) }
+//            },
+//            removeAvatar = ::removeProfilePicture
+//        )
+//
+//        updateAvatarDialogImage(viewModel.temporaryAvatar.value)
+//    }
+
+//    private fun updateAvatarDialogImage(temporaryAvatar: ByteArray?){
+//        avatarDialog?.update(
+//            temporaryAvatar = temporaryAvatar,
+//            hasUserAvatar = viewModel.hasAvatar()
+//        )
+//    }
+
     // Helper method used by updateProfilePicture and removeProfilePicture to sync it online
     private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
         binding.loader.isVisible = true
@@ -415,39 +438,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
         return updateDisplayName(displayName)
     }
 
-    private fun showEditProfilePictureUI() {
-        showSessionDialog {
-            title(R.string.profileDisplayPictureSet)
-            view(R.layout.dialog_change_avatar)
-
-            // Note: This is the only instance in a dialog where the "Save" button is not a `dangerButton`
-            button(R.string.save) { startAvatarSelection() }
-
-            if (prefs.getProfileAvatarId() != 0) {
-                button(R.string.remove) { removeProfilePicture() }
-            }
-            cancelButton()
-        }.apply {
-            val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
-                ?.also(::setupProfilePictureView)
-
-            val pictureIcon = findViewById<View>(R.id.ic_pictures)
-
-            val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
-
-            val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
-
-            profilePic?.isVisible = photoSet
-            pictureIcon?.isVisible = !photoSet
-        }
-    }
-
     private fun startAvatarSelection() {
         // Ask for an optional camera permission.
         Permissions.with(this)
             .request(Manifest.permission.CAMERA)
             .onAnyResult {
-                tempFile = avatarSelection.startAvatarSelection( false, true)
+                avatarSelection.startAvatarSelection(
+                    includeClear = false,
+                    attemptToIncludeCamera = true,
+                    createTempFile = viewModel::createTempFile
+                )
             }
             .execute()
     }
@@ -574,6 +574,124 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
             }
         }
     }
+
+    @Composable
+    fun AvatarDialogContainer(
+        startAvatarSelection: ()->Unit,
+        saveAvatar: ()->Unit,
+        removeAvatar: ()->Unit
+    ){
+        val state by viewModel.avatarDialogState.collectAsState()
+
+        AvatarDialog(
+            state = state,
+            startAvatarSelection = startAvatarSelection,
+            saveAvatar = saveAvatar,
+            removeAvatar = removeAvatar
+        )
+    }
+
+    @Composable
+    fun AvatarDialog(
+        state: SettingsViewModel.AvatarDialogState,
+        startAvatarSelection: ()->Unit,
+        saveAvatar: ()->Unit,
+        removeAvatar: ()->Unit
+    ){
+        AlertDialog(
+            onDismissRequest = {
+                viewModel.onAvatarDialogDismissed()
+                showAvatarDialog = false
+            },
+            title = stringResource(R.string.profileDisplayPictureSet),
+            content = {
+                Box(
+                    modifier = Modifier
+                        .padding(top = LocalDimensions.current.smallSpacing)
+
+                        .size(dimensionResource(id = R.dimen.large_profile_picture_size))
+                        .clickable {
+                            startAvatarSelection()
+                        }
+                        .background(
+                            shape = CircleShape,
+                            color = LocalColors.current.backgroundBubbleReceived,
+                        ),
+                    contentAlignment = Alignment.Center
+                ) {
+                    when(val s = state){
+                        // user avatar
+                        is UserAvatar -> {
+                            Avatar(userAddress = s.address)
+                        }
+
+                        // temporary image
+                        is TempAvatar -> {
+                            Image(
+                                modifier = Modifier.size(dimensionResource(id = R.dimen.large_profile_picture_size))
+                                    .clip(shape = CircleShape,),
+                                bitmap = BitmapFactory.decodeByteArray(s.data, 0, s.data.size).asImageBitmap(),
+                                contentDescription = null
+                            )
+                        }
+
+                        // empty state
+                        else -> {
+                            Image(
+                                modifier = Modifier.align(Alignment.Center),
+                                painter = painterResource(id = R.drawable.ic_pictures),
+                                contentDescription = null,
+                                colorFilter = ColorFilter.tint(LocalColors.current.textSecondary)
+                            )
+                        }
+                    }
+
+                    Image(
+                        modifier = Modifier
+                            .size(LocalDimensions.current.spacing)
+                            .background(
+                                shape = CircleShape,
+                                color = LocalColors.current.primary
+                            )
+                            .padding(LocalDimensions.current.xxxsSpacing)
+                            .align(Alignment.BottomEnd)
+                        ,
+                        painter = painterResource(id = R.drawable.ic_plus),
+                        contentDescription = null,
+                        colorFilter = ColorFilter.tint(Color.Black)
+                    )
+                }
+            },
+            showCloseButton = true, // display the 'x' button
+            buttons = listOf(
+                DialogButtonModel(
+                    text = GetString(R.string.save),
+                    contentDescription = GetString(R.string.AccessibilityId_save),
+                    onClick = saveAvatar
+                ),
+                DialogButtonModel(
+                    text = GetString(R.string.remove),
+                    contentDescription = GetString(R.string.AccessibilityId_remove),
+                    onClick = removeAvatar
+                )
+            )
+        )
+    }
+
+    @Preview
+    @Composable
+    fun PreviewAvatarDialog(
+        @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
+    ){
+        PreviewTheme(colors) {
+            AvatarDialog(
+                state = NoAvatar,
+                startAvatarSelection = {},
+                saveAvatar = {},
+                removeAvatar = {}
+            )
+        }
+    }
 }
 
 private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt
new file mode 100644
index 0000000000..c09842484b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt
@@ -0,0 +1,215 @@
+package org.thoughtcrime.securesms.preferences
+
+import android.content.Context
+import android.widget.Toast
+import androidx.core.view.isVisible
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewModelScope
+import com.canhub.cropper.CropImage
+import com.canhub.cropper.CropImageView
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import network.loki.messenger.libsession_util.util.UserPic
+import nl.komponents.kovenant.ui.alwaysUi
+import nl.komponents.kovenant.ui.failUi
+import nl.komponents.kovenant.ui.successUi
+import org.session.libsession.avatars.AvatarHelper
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.ProfileKeyUtil
+import org.session.libsession.utilities.ProfilePictureUtilities
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.truncateIdForDisplay
+import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
+import org.session.libsignal.utilities.Log
+import org.session.libsignal.utilities.NoExternalStorageException
+import org.session.libsignal.utilities.Util.SECURE_RANDOM
+import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
+import org.thoughtcrime.securesms.util.BitmapDecodingException
+import org.thoughtcrime.securesms.util.BitmapUtil
+import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
+import org.thoughtcrime.securesms.util.NetworkUtils
+import java.io.File
+import java.io.IOException
+import javax.inject.Inject
+
+@HiltViewModel
+class SettingsViewModel @Inject constructor(
+    @ApplicationContext private val context: Context,
+    val prefs: TextSecurePreferences
+) : ViewModel() {
+    private val TAG = "SettingsViewModel"
+
+    private var tempFile: File? = null
+
+    val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
+
+    private val _avatarDialogState: MutableStateFlow<AvatarDialogState> = MutableStateFlow(
+        getDefaultAvatarDialogState()
+    )
+    val avatarDialogState: StateFlow<AvatarDialogState>
+        get() = _avatarDialogState
+
+    fun getDisplayName(): String =
+        prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
+
+    fun hasAvatar() = prefs.getProfileAvatarId() != 0
+
+    fun createTempFile(): File? {
+        try {
+            tempFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(context))
+        } catch (e: IOException) {
+            Log.e("Cannot reserve a temporary avatar capture file.", e)
+        } catch (e: NoExternalStorageException) {
+            Log.e("Cannot reserve a temporary avatar capture file.", e)
+        }
+
+        return tempFile
+    }
+
+    fun getTempFile() = tempFile
+
+    fun onAvatarPicked(result: CropImageView.CropResult) {
+        when {
+            result.isSuccessful -> {
+                Log.i(TAG, result.getUriFilePath(context).toString())
+
+                viewModelScope.launch(Dispatchers.IO) {
+                    try {
+                        val profilePictureToBeUploaded =
+                            BitmapUtil.createScaledBytes(
+                                context,
+                                result.getUriFilePath(context).toString(),
+                                ProfileMediaConstraints()
+                            ).bitmap
+
+                        // update dialog with temporary avatar (has not been saved/uploaded yet)
+                        _avatarDialogState.value =
+                            AvatarDialogState.TempAvatar(profilePictureToBeUploaded)
+                    } catch (e: BitmapDecodingException) {
+                        Log.e(TAG, e)
+                    }
+                }
+            }
+
+            result is CropImage.CancelledResult -> {
+                Log.i(TAG, "Cropping image was cancelled by the user")
+            }
+
+            else -> {
+                Log.e(TAG, "Cropping image failed")
+            }
+        }
+    }
+
+    fun onAvatarDialogDismissed() {
+        _avatarDialogState.value =getDefaultAvatarDialogState()
+    }
+
+    fun getDefaultAvatarDialogState() = if (hasAvatar()) AvatarDialogState.UserAvatar(Address.fromSerialized(hexEncodedPublicKey))
+    else AvatarDialogState.NoAvatar
+
+    //todo properly close dialog when done and make sure the state is the right one post change
+    //todo make ripple effect round in dialog avatar picker
+    //todo link other states, like making sure we show the actual avatar if there's already one
+    //todo move upload and remove to VM
+    //todo make buttons in dialog disabled
+    //todo clean up the classes I made which aren't used now...
+
+    sealed class AvatarDialogState() {
+        object NoAvatar : AvatarDialogState()
+        data class UserAvatar(val address: Address) : AvatarDialogState()
+        data class TempAvatar(val data: ByteArray) : AvatarDialogState()
+    }
+
+    // Helper method used by updateProfilePicture and removeProfilePicture to sync it online
+    /*private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
+        binding.loader.isVisible = true
+
+        // Grab the profile key and kick of the promise to update the profile picture
+        val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
+        val updateProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)
+
+        // If the online portion of the update succeeded then update the local state
+        updateProfilePicturePromise.successUi {
+
+            // When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
+            if (profilePicture.isEmpty()) {
+                MessagingModuleConfiguration.shared.storage.clearUserPic()
+            }
+
+            val userConfig = configFactory.user
+            AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
+            prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() )
+            ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
+
+            // Attempt to grab the details we require to update the profile picture
+            val url = prefs.getProfilePictureURL()
+            val profileKey = ProfileKeyUtil.getProfileKey(this)
+
+            // If we have a URL and a profile key then set the user's profile picture
+            if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
+                userConfig?.setPic(UserPic(url, profileKey))
+            }
+
+            if (userConfig != null && userConfig.needsDump()) {
+                configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
+            }
+
+            ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
+
+            // Update our visuals
+            binding.profilePictureView.recycle()
+            binding.profilePictureView.update()
+        }
+
+        // If the sync failed then inform the user
+        updateProfilePicturePromise.failUi { onFail() }
+
+        // Finally, remove the loader animation after we've waited for the attempt to succeed or fail
+        updateProfilePicturePromise.alwaysUi { binding.loader.isVisible = false }
+    }
+
+    private fun updateProfilePicture(profilePicture: ByteArray) {
+
+        val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
+        if (!haveNetworkConnection) {
+            Log.w(TAG, "Cannot update profile picture - no network connection.")
+            Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
+            return
+        }
+
+        val onFail: () -> Unit = {
+            Log.e(TAG, "Sync failed when uploading profile picture.")
+            Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
+        }
+
+        syncProfilePicture(profilePicture, onFail)
+    }
+
+    private fun removeProfilePicture() {
+
+        val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
+        if (!haveNetworkConnection) {
+            Log.w(TAG, "Cannot remove profile picture - no network connection.")
+            Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
+            return
+        }
+
+        val onFail: () -> Unit = {
+            Log.e(TAG, "Sync failed when removing profile picture.")
+            Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
+        }
+
+        val emptyProfilePicture = ByteArray(0)
+        syncProfilePicture(emptyProfilePicture, onFail)
+    }*/
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
index d04d73fda1..355d74947d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
@@ -65,6 +65,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import network.loki.messenger.R
+import org.session.libsession.utilities.Address
 import org.session.libsession.utilities.recipients.Recipient
 import org.thoughtcrime.securesms.components.ProfilePictureView
 import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCardData
@@ -399,22 +400,31 @@ fun Divider(modifier: Modifier = Modifier, startIndent: Dp = 0.dp) {
     )
 }
 
+//TODO This component should be fully rebuilt in Compose at some point ~~
 @Composable
-fun RowScope.Avatar(recipient: Recipient) {
-    Box(
-        modifier = Modifier
-            .width(60.dp)
-            .align(Alignment.CenterVertically)
-    ) {
-        AndroidView(
-            factory = {
-                ProfilePictureView(it).apply { update(recipient) }
-            },
-            modifier = Modifier
-                .width(46.dp)
-                .height(46.dp)
-        )
-    }
+fun Avatar(
+    recipient: Recipient,
+    modifier: Modifier = Modifier
+) {
+    AndroidView(
+        factory = {
+            ProfilePictureView(it).apply { update(recipient) }
+        },
+        modifier = modifier
+    )
+}
+
+@Composable
+fun Avatar(
+    userAddress: Address,
+    modifier: Modifier = Modifier
+) {
+    AndroidView(
+        factory = {
+            ProfilePictureView(it).apply { update(userAddress) }
+        },
+        modifier = modifier
+    )
 }
 
 @Composable
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index cd5e2e7cef..3bfc9c65b0 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -139,4 +139,9 @@
 
     </FrameLayout>
 
+    <androidx.compose.ui.platform.ComposeView
+        android:id="@+id/avatarDialog"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
 </RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_change_avatar.xml b/app/src/main/res/layout/dialog_change_avatar.xml
deleted file mode 100644
index f240f81bbc..0000000000
--- a/app/src/main/res/layout/dialog_change_avatar.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    android:orientation="vertical"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_height="wrap_content"
-    android:layout_width="match_parent">
-
-    <FrameLayout
-        android:layout_gravity="center"
-        android:id="@+id/ic_pictures"
-        android:background="@drawable/circle_tintable"
-        android:backgroundTint="@color/classic_dark_3"
-        android:layout_marginTop="15dp"
-        android:layout_marginBottom="15dp"
-        android:layout_width="@dimen/large_profile_picture_size"
-        android:layout_height="@dimen/large_profile_picture_size">
-
-        <ImageView
-            android:layout_gravity="center"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:background="@color/transparent"
-            android:src="@drawable/ic_pictures"/>
-
-        <!-- TODO: Add this back when we build the custom modal which allows tapping on the image to select a replacement-->
-<!--        <LinearLayout-->
-<!--            android:layout_gravity="bottom|end"-->
-<!--            android:gravity="center"-->
-<!--            android:background="@drawable/circle_tintable"-->
-<!--            android:backgroundTint="?attr/accentColor"-->
-<!--            android:paddingTop="1dp"-->
-<!--            android:paddingLeft="1dp"-->
-<!--            android:layout_width="24dp"-->
-<!--            android:layout_height="24dp"-->
-<!--            tools:backgroundTint="@color/accent_green">-->
-<!--            <View-->
-<!--                android:background="@drawable/ic_plus"-->
-<!--                android:backgroundTint="@color/black"-->
-<!--                android:layout_width="12dp"-->
-<!--                android:layout_height="12dp"-->
-<!--                />-->
-<!--        </LinearLayout>-->
-
-
-    </FrameLayout>
-
-    <org.thoughtcrime.securesms.components.ProfilePictureView
-        android:layout_margin="30dp"
-        android:id="@+id/profile_picture_view"
-        android:layout_gravity="center"
-        android:layout_width="@dimen/large_profile_picture_size"
-        android:layout_height="@dimen/large_profile_picture_size"
-        android:layout_marginTop="@dimen/medium_spacing"
-        android:contentDescription="@string/AccessibilityId_profilePicture"
-        tools:visibility="gone"/>
-
-</FrameLayout>
diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml
index 4dae1db1c5..5eaab10ae0 100644
--- a/content-descriptions/src/main/res/values/strings.xml
+++ b/content-descriptions/src/main/res/values/strings.xml
@@ -43,6 +43,7 @@
     <string name="AccessibilityId_contactUserDetails">Details</string>
     <string name="AccessibilityId_pin">Pin</string>
     <string name="AccessibilityId_profilePicture">User settings</string>
+    <string name="AccessibilityId_avatarPicker">Image picker</string>
     <string name="AccessibilityId_searchIcon">Search icon</string>
     <!--Settings Page -->
     <string name="AccessibilityId_conversationsBlockedContacts">Blocked contacts</string>
@@ -122,7 +123,7 @@
     <string name="AccessibilityId_message">Message body</string>
     <string name="AccessibilityId_sent">Message sent status: Sent</string>
     <string name="AccessibilityId_reply">Reply to message</string>
-    <string name="AccessibilityId_save">Save attachment</string>
+    <string name="AccessibilityId_saveAttachment">Save attachment</string>
     <string name="AccessibilityId_select">Select</string>
     <string name="AccessibilityId_messageVoice">Voice message</string>
     <string name="AccessibilityId_deliveryIndicator">Delivered</string>
@@ -157,5 +158,7 @@
     <string name="AccessibilityId_close">Close Dialog</string>
     <string name="AccessibilityId_expand">Expand</string>
     <string name="AccessibilityId_mediaMessage">Media message</string>
+    <string name="AccessibilityId_save">Save</string>
+    <string name="AccessibilityId_remove">Remove</string>
 
 </resources>
\ No newline at end of file