diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessage.kt
new file mode 100644
index 0000000000..d262d818fd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessage.kt
@@ -0,0 +1,123 @@
+package org.thoughtcrime.securesms.conversation.newmessage
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.ui.LoadingArcOr
+import org.thoughtcrime.securesms.ui.LocalDimensions
+import org.thoughtcrime.securesms.ui.PreviewTheme
+import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
+import org.thoughtcrime.securesms.ui.color.Colors
+import org.thoughtcrime.securesms.ui.color.LocalColors
+import org.thoughtcrime.securesms.ui.components.AppBar
+import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
+import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
+import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
+import org.thoughtcrime.securesms.ui.components.SessionTabRow
+import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
+import org.thoughtcrime.securesms.ui.contentDescription
+
+private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun NewMessage(
+    state: State,
+    errors: Flow<String> = emptyFlow(),
+    callbacks: Callbacks = object: Callbacks {},
+    onClose: () -> Unit = {},
+    onBack: () -> Unit = {},
+    onHelp: () -> Unit = {},
+) {
+    val pagerState = rememberPagerState { TITLES.size }
+
+    Column(modifier = Modifier.background(LocalColors.current.backgroundSecondary)) {
+        AppBar(stringResource(R.string.messageNew), onClose = onClose, onBack = onBack)
+        SessionTabRow(pagerState, TITLES)
+        HorizontalPager(pagerState) {
+            when (TITLES[it]) {
+                R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
+                R.string.qrScan -> MaybeScanQrCode(errors, onScan = callbacks::onScanQrCode)
+            }
+        }
+    }
+}
+
+@Composable
+private fun EnterAccountId(
+    state: State,
+    callbacks: Callbacks,
+    onHelp: () -> Unit = {}
+) {
+    Column(
+        modifier = Modifier
+            .padding(horizontal = LocalDimensions.current.marginExtraExtraSmall, vertical = LocalDimensions.current.marginExtraSmall)
+            .fillMaxHeight(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.marginExtraSmall)
+    ) {
+        SessionOutlinedTextField(
+            text = state.newMessageIdOrOns,
+            modifier = Modifier
+                .padding(horizontal = LocalDimensions.current.marginSmall)
+                .contentDescription("Session id input box"),
+            placeholder = stringResource(R.string.accountIdOrOnsEnter),
+            onChange = callbacks::onChange,
+            onContinue = callbacks::onContinue,
+            error = state.error?.string(),
+        )
+
+        BorderlessButtonWithIcon(
+            text = stringResource(R.string.messageNewDescription),
+            iconRes = R.drawable.ic_circle_question_mark,
+            contentColor = LocalColors.current.textSecondary,
+            modifier = Modifier
+                .animateContentSize()
+                .contentDescription(R.string.AccessibilityId_help_desk_link)
+                .padding(horizontal = LocalDimensions.current.marginMedium)
+                .fillMaxWidth(),
+        ) { onHelp() }
+
+        SlimOutlineButton(
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally)
+                .padding(horizontal = LocalDimensions.current.marginLarge)
+                .fillMaxWidth()
+                .contentDescription(R.string.next),
+            color = LocalColors.current.primary,
+            enabled = state.isNextButtonEnabled,
+            onClick = { callbacks.onContinue() }
+        ) {
+            LoadingArcOr(state.loading) {
+                Text(stringResource(R.string.next))
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun PreviewNewMessage(
+    @PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
+) {
+    PreviewTheme(colors) {
+        NewMessage(State())
+    }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageFragment.kt
index 23f3c46d99..3dd72f3528 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageFragment.kt
@@ -5,66 +5,25 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.expandIn
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.animation.shrinkOut
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.filterIsInstance
 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.conversation.newmessage.Callbacks
-import org.thoughtcrime.securesms.conversation.newmessage.Event
+import org.thoughtcrime.securesms.conversation.newmessage.NewMessage
 import org.thoughtcrime.securesms.conversation.newmessage.NewMessageViewModel
 import org.thoughtcrime.securesms.conversation.newmessage.State
 import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
 import org.thoughtcrime.securesms.dependencies.DatabaseComponent
 import org.thoughtcrime.securesms.showOpenUrlDialog
-import org.thoughtcrime.securesms.ui.color.Colors
-import org.thoughtcrime.securesms.ui.LoadingArcOr
-import org.thoughtcrime.securesms.ui.color.LocalColors
-import org.thoughtcrime.securesms.ui.LocalDimensions
-import org.thoughtcrime.securesms.ui.PreviewTheme
-import org.thoughtcrime.securesms.ui.SessionColorsParameterProvider
-import org.thoughtcrime.securesms.ui.components.AppBar
-import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
-import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
-import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
-import org.thoughtcrime.securesms.ui.components.SessionTabRow
-import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
-import org.thoughtcrime.securesms.ui.contentDescription
 import org.thoughtcrime.securesms.ui.createThemedComposeView
 
 class NewMessageFragment : Fragment() {
-
-    val viewModel: NewMessageViewModel by viewModels()
+    private val viewModel: NewMessageViewModel by viewModels()
 
     lateinit var delegate: NewConversationDelegate
 
@@ -72,8 +31,8 @@ class NewMessageFragment : Fragment() {
         super.onCreate(savedInstanceState)
 
         lifecycleScope.launch {
-            viewModel.event.filterIsInstance<Event.Success>().collect {
-                createPrivateChat(it.key)
+            viewModel.success.collect {
+                createPrivateChat(it.publicKey)
             }
         }
     }
@@ -103,91 +62,3 @@ class NewMessageFragment : Fragment() {
         delegate.onDialogClosePressed()
     }
 }
-
-@Preview
-@Composable
-private fun PreviewNewMessage(
-    @PreviewParameter(SessionColorsParameterProvider::class) colors: Colors
-) {
-    PreviewTheme(colors) {
-        NewMessage(State())
-    }
-}
-
-private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun NewMessage(
-    state: State,
-    errors: Flow<String> = emptyFlow(),
-    callbacks: Callbacks = object: Callbacks {},
-    onClose: () -> Unit = {},
-    onBack: () -> Unit = {},
-    onHelp: () -> Unit = {},
-) {
-    val pagerState = rememberPagerState { TITLES.size }
-
-    Column(modifier = Modifier.background(LocalColors.current.backgroundSecondary)) {
-        AppBar(stringResource(R.string.messageNew), onClose = onClose, onBack = onBack)
-        SessionTabRow(pagerState, TITLES)
-        HorizontalPager(pagerState) {
-            when (TITLES[it]) {
-                R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
-                R.string.qrScan -> MaybeScanQrCode(errors, onScan = callbacks::onScanQrCode)
-            }
-        }
-    }
-}
-
-@Composable
-fun EnterAccountId(
-    state: State,
-    callbacks: Callbacks,
-    onHelp: () -> Unit = {}
-) {
-    Column(
-        modifier = Modifier
-            .padding(horizontal = LocalDimensions.current.marginExtraExtraSmall, vertical = LocalDimensions.current.marginExtraSmall)
-            .fillMaxHeight(),
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.marginExtraSmall)
-    ) {
-        SessionOutlinedTextField(
-            text = state.newMessageIdOrOns,
-            modifier = Modifier
-                .padding(horizontal = LocalDimensions.current.marginSmall)
-                .contentDescription("Session id input box"),
-            placeholder = stringResource(R.string.accountIdOrOnsEnter),
-            onChange = callbacks::onChange,
-            onContinue = callbacks::onContinue,
-            error = state.error?.string(),
-        )
-
-        BorderlessButtonWithIcon(
-            text = stringResource(R.string.messageNewDescription),
-            iconRes = R.drawable.ic_circle_question_mark,
-            contentColor = LocalColors.current.textSecondary,
-            modifier = Modifier
-                .animateContentSize()
-                .contentDescription(R.string.AccessibilityId_help_desk_link)
-                .padding(horizontal = LocalDimensions.current.marginMedium)
-                .fillMaxWidth(),
-        ) { onHelp() }
-
-        SlimOutlineButton(
-            modifier = Modifier
-                .align(Alignment.CenterHorizontally)
-                .padding(horizontal = LocalDimensions.current.marginLarge)
-                .fillMaxWidth()
-                .contentDescription(R.string.next),
-            color = LocalColors.current.primary,
-            enabled = state.isNextButtonEnabled,
-            onClick = { callbacks.onContinue() }
-        ) {
-            LoadingArcOr(state.loading) {
-                Text(stringResource(R.string.next))
-            }
-        }
-    }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageViewModel.kt
index a0c6ba50f8..8551722c15 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/newmessage/NewMessageViewModel.kt
@@ -5,37 +5,34 @@ import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.timeout
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
 import network.loki.messenger.R
 import org.session.libsession.snode.SnodeAPI
 import org.session.libsignal.utilities.PublicKeyValidation
-import org.session.libsignal.utilities.asFlow
 import org.thoughtcrime.securesms.ui.GetString
 import javax.inject.Inject
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.time.Duration.Companion.seconds
 
 @HiltViewModel
-class NewMessageViewModel @Inject constructor(
+internal class NewMessageViewModel @Inject constructor(
     private val application: Application
 ): AndroidViewModel(application), Callbacks {
 
     private val _state = MutableStateFlow(State())
     val state = _state.asStateFlow()
 
-    private val _event = Channel<Event>()
-    val event: Flow<Event> get() = _event.receiveAsFlow()
+    private val _success = Channel<Success>()
+    val success: Flow<Success> get() = _success.receiveAsFlow()
 
     private val _qrErrors = Channel<String>()
     val qrErrors: Flow<String> = _qrErrors.receiveAsFlow()
@@ -50,7 +47,13 @@ class NewMessageViewModel @Inject constructor(
     }
 
     override fun onContinue() {
-        createPrivateChatIfPossible(state.value.newMessageIdOrOns)
+        val idOrONS = state.value.newMessageIdOrOns
+
+        if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) {
+            onUnvalidatedPublicKey(idOrONS)
+        } else {
+            resolveONS(idOrONS)
+        }
     }
 
     override fun onScanQrCode(value: String) {
@@ -61,38 +64,23 @@ class NewMessageViewModel @Inject constructor(
         }
     }
 
-    @OptIn(FlowPreview::class)
-    private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
+    private fun resolveONS(ons: String) {
         if (loadOnsJob?.isActive == true) return
 
-        if (PublicKeyValidation.isValid(onsNameOrPublicKey, isPrefixRequired = false)) {
-            if (PublicKeyValidation.hasValidPrefix(onsNameOrPublicKey)) {
-                onPublicKey(onsNameOrPublicKey)
-            } else {
-                _state.update { it.copy(error = GetString(R.string.accountIdErrorInvalid), loading = false) }
-            }
-        } else {
-            // This could be an ONS name
-            _state.update { it.copy(error = null, loading = true) }
-
-            loadOnsJob = viewModelScope.launch(Dispatchers.IO) {
-                try {
-                    // TODO move timeout to SnodeAPI#getSessionID
-                    SnodeAPI.getSessionID(onsNameOrPublicKey).asFlow()
-                        .timeout(30.seconds)
-                        .collectLatest {
-                            _state.update { it.copy(loading = false) }
-                            onPublicKey(onsNameOrPublicKey)
-                        }
-                } catch (e: TimeoutCancellationException) {
-                    onError(e)
-                } catch (e: CancellationException) {
-                    // Ignore JobCancellationException, which is called when we cancel the job and
-                    // is handled where the job is canceled.
-                    // Can't reference JobCancellationException directly, it is internal.
-                } catch (e: Exception) {
-                    onError(e)
-                }
+        // This could be an ONS name
+        _state.update { it.copy(error = null, loading = true) }
+
+        loadOnsJob = viewModelScope.launch(Dispatchers.IO) {
+            try {
+                val publicKey = withTimeout(30.seconds) { SnodeAPI.getSessionID(ons).get() }
+                onPublicKey(publicKey)
+            } catch (e: TimeoutCancellationException) {
+                onError(e)
+            } catch (e: CancellationException) {
+                // Attempting to just ignore internal JobCancellationException, which is called
+                // when we cancel the job, state update is handled there.
+            } catch (e: Exception) {
+                onError(e)
             }
         }
     }
@@ -101,8 +89,17 @@ class NewMessageViewModel @Inject constructor(
         _state.update { it.copy(loading = false, error = GetString(e) { it.toMessage() }) }
     }
 
-    private fun onPublicKey(onsNameOrPublicKey: String) {
-        viewModelScope.launch { _event.send(Event.Success(onsNameOrPublicKey)) }
+    private fun onPublicKey(publicKey: String) {
+        _state.update { it.copy(loading = false) }
+        viewModelScope.launch { _success.send(Success(publicKey)) }
+    }
+
+    private fun onUnvalidatedPublicKey(publicKey: String) {
+        if (PublicKeyValidation.hasValidPrefix(publicKey)) {
+            onPublicKey(publicKey)
+        } else {
+            _state.update { it.copy(error = GetString(R.string.accountIdErrorInvalid), loading = false) }
+        }
     }
 
     private fun Exception.toMessage() = when (this) {
@@ -111,7 +108,7 @@ class NewMessageViewModel @Inject constructor(
     }
 }
 
-data class State(
+internal data class State(
     val newMessageIdOrOns: String = "",
     val error: GetString? = null,
     val loading: Boolean = false
@@ -119,6 +116,4 @@ data class State(
     val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank()
 }
 
-sealed interface Event {
-    data class Success(val key: String): Event
-}
+internal data class Success(val publicKey: String)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt
index f80acee64e..60b5fb8b2c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/RadioOptionAdapter.kt
@@ -10,7 +10,6 @@ import androidx.recyclerview.widget.ListAdapter
 import androidx.recyclerview.widget.RecyclerView
 import network.loki.messenger.R
 import network.loki.messenger.databinding.ItemSelectableBinding
-import network.loki.messenger.libsession_util.util.ExpiryMode
 import org.thoughtcrime.securesms.mms.GlideApp
 import org.thoughtcrime.securesms.ui.GetString
 import java.util.Objects
@@ -68,7 +67,6 @@ class RadioOptionAdapter<T>(
             }
         }
     }
-
 }
 
 data class RadioOption<out T>(
diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt
index b2e187f134..fdf8f107b9 100644
--- a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt
+++ b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt
@@ -1,9 +1,6 @@
 @file:JvmName("PromiseUtilities")
 package org.session.libsignal.utilities
 
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.isActive
 import nl.komponents.kovenant.Promise
 import nl.komponents.kovenant.deferred
 import nl.komponents.kovenant.functional.map
@@ -70,19 +67,3 @@ infix fun <V, E: Exception> Promise<V, E>.sideEffect(
     callback(it)
     it
 }
-
-/**
- * Observe a [Promise] as a flow
- *
- * Warning: Promise will not be canceled on unsubscribe.
- */
-fun <V, E: Exception> Promise<V, E>.asFlow() = callbackFlow {
-    success {
-        if (isActive) trySend(it)
-        close()
-    } fail {
-        close(it)
-    }
-
-    awaitClose()
-}