diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ce76de39a8dfd5d9a6dcffa07a3b37de2721bd34..14daa9aa93efc2020a96f0e7ded0931840699bf0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -56,9 +56,18 @@
             android:theme="@style/SplashTheme">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <intent-filter android:autoVerify="true">
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <action android:name="android.intent.action.VIEW" />
+                <data
+                    android:scheme="https"
+                    android:host="elixxir.io"
+                    android:pathPrefix="/connect"
+                    />
+            </intent-filter>
         </activity>
         <activity
             android:name=".ui.intro.splash.SplashScreenLoadingActivity"
@@ -84,9 +93,7 @@
             android:launchMode="singleTask">
             <intent-filter>
                 <data android:scheme="db-${dropboxKey}" />
-
                 <action android:name="android.intent.action.VIEW" />
-
                 <category android:name="android.intent.category.BROWSABLE" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
diff --git a/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt b/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt
index 45fc38d723c61ddd7425f49d9ee6465e286e5893..617601e34341ea387629bbd3cec3cb4c940ec06d 100644
--- a/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt
+++ b/app/src/main/java/io/xxlabs/messenger/data/datatype/RequestStatus.kt
@@ -13,7 +13,8 @@ enum class RequestStatus(val value: Int) {
     RESENT(7),
     SENDING(10),
     DELETING(11),
-    HIDDEN(12);
+    HIDDEN(12),
+    SEARCH(99);
 
     companion object {
         fun from(value: Int) = values().first { it.value == value }
diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt b/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt
index 7372cde628632a85aaa673cee0ca08e06ba30716..b3ab9ba44e9af59c52ccbf075551e030ea1efc19 100644
--- a/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt
+++ b/app/src/main/java/io/xxlabs/messenger/data/room/dao/ContactsDao.kt
@@ -43,9 +43,6 @@ interface ContactsDao {
     @Query("SELECT * FROM Contacts WHERE status = :status")
     fun getAllContactsWithStatusLive(status: Int): LiveData<List<ContactData>>
 
-    @Query("SELECT * FROM Contacts WHERE username LIKE :username ORDER BY username")
-    fun queryAllContactsUsername(username: String): Single<List<ContactData>>
-
     @Query("SELECT * FROM Contacts WHERE username = :username LIMIT 1")
     fun queryContactByUsername(username: String): Maybe<ContactData>
 
diff --git a/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt b/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt
index 57ba12e58c2cfa4f3c65655edc9b1aef8c03bf19..45fb236e55c34b0cc41ea1920e756dd75c2f5745 100644
--- a/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt
+++ b/app/src/main/java/io/xxlabs/messenger/data/room/model/Contact.kt
@@ -32,8 +32,7 @@ fun Contact.formattedEmail(): String? =
     else null
 
 fun Contact.formattedPhone(flagEmoji: Boolean = false): String? =
-    if (phone.isNotBlank()) Country.toFormattedNumber(phone, flagEmoji)
-    else null
+    phone.ifBlank { null }
 
 suspend fun Contact.resolveBitmap(): Bitmap? = withContext(Dispatchers.IO) {
     BitmapResolver.getBitmap(photo)
diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt
index 51204ffd73769bfe80c0e0723542489a2bdfbcd0..a59f54ebd354cb01967a20d48484c7d9868c0b6e 100644
--- a/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt
+++ b/app/src/main/java/io/xxlabs/messenger/di/modules/FragmentMainBuildersModule.kt
@@ -17,6 +17,7 @@ import io.xxlabs.messenger.requests.ui.list.ReceivedRequestsFragment
 import io.xxlabs.messenger.requests.ui.list.SentRequestsFragment
 import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameDialog
 import io.xxlabs.messenger.requests.ui.send.SendRequestDialog
+import io.xxlabs.messenger.search.*
 import io.xxlabs.messenger.ui.main.chat.PrivateMessagesFragment
 import io.xxlabs.messenger.ui.main.chats.ChatsFragment
 import io.xxlabs.messenger.ui.main.contacts.list.ContactListFragment
@@ -134,4 +135,22 @@ abstract class FragmentMainBuildersModule {
 
     @ContributesAndroidInjector
     abstract fun contributeInvitationAcceptedDialog(): InvitationAcceptedDialog
+
+    @ContributesAndroidInjector
+    abstract fun contributeUserSearchFragment(): UserSearchFragment
+
+    @ContributesAndroidInjector
+    abstract fun contributeFactSearchFragment(): FactSearchFragment
+
+    @ContributesAndroidInjector
+    abstract fun contributeUsernameSearchFragment(): UsernameSearchFragment
+
+    @ContributesAndroidInjector
+    abstract fun contributeEmailSearchFragment(): EmailSearchFragment
+
+    @ContributesAndroidInjector
+    abstract fun contributePhoneSearchFragment(): PhoneSearchFragment
+
+    @ContributesAndroidInjector
+    abstract fun contributeQrSearchFragment(): QrSearchFragment
 }
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt b/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt
index b30ff23dbfcea15296d7b079ce648e2a0988e071..bd58694cb27bdf8e574c7b347a436f0f57b6098e 100644
--- a/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt
+++ b/app/src/main/java/io/xxlabs/messenger/di/modules/ViewModelModule.kt
@@ -8,6 +8,7 @@ import dagger.multibindings.IntoMap
 import io.xxlabs.messenger.di.utils.DaggerViewModelFactory
 import io.xxlabs.messenger.di.utils.ViewModelKey
 import io.xxlabs.messenger.requests.ui.RequestsViewModel
+import io.xxlabs.messenger.search.UserSearchViewModel
 import io.xxlabs.messenger.ui.base.ContactDetailsViewModel
 import io.xxlabs.messenger.ui.global.ContactsViewModel
 import io.xxlabs.messenger.ui.global.NetworkViewModel
@@ -97,6 +98,11 @@ abstract class ViewModelModule {
     @ViewModelKey(ConnectionsViewModel::class)
     abstract fun bindConnectionsViewModel(connectionsViewModel: ConnectionsViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(UserSearchViewModel::class)
+    abstract fun bindUserSearchViewModel(userSearchViewModel: UserSearchViewModel): ViewModel
+
     @Binds
     abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
 }
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt b/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt
index c098e3e84d90ae85339ec8be3a8bd2c70488e355..8cbd1f143de2593be126e3a6c2574ad5c1697fc9 100644
--- a/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt
+++ b/app/src/main/java/io/xxlabs/messenger/notifications/MessagingService.kt
@@ -5,7 +5,6 @@ import android.app.NotificationChannel
 import android.app.NotificationManager
 import android.app.PendingIntent
 import android.content.Intent
-import android.media.RingtoneManager.isDefault
 import android.os.Build
 import android.os.Bundle
 import android.os.PowerManager
@@ -28,7 +27,7 @@ import io.xxlabs.messenger.requests.ui.RequestsFragment
 import io.xxlabs.messenger.support.util.value
 import io.xxlabs.messenger.ui.intro.splash.SplashScreenPlaceholderActivity
 import io.xxlabs.messenger.ui.main.MainActivity
-import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_DEEP_LINK_BUNDLE
+import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_NOTIFICATION_CLICK
 import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_GROUP_CHAT
 import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_PRIVATE_CHAT
 import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_REQUEST
@@ -196,7 +195,7 @@ class MessagingService : FirebaseMessagingService(), HasAndroidInjector {
                 }
             }
         }
-        intent.putExtra(INTENT_DEEP_LINK_BUNDLE, deepLinkBundle)
+        intent.putExtra(INTENT_NOTIFICATION_CLICK, deepLinkBundle)
         return intent
     }
 
diff --git a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
index 4fb123e5e9991d3937cb78b0a51f3ae0b638c9d0..5d3d5c48905e602ac92ca6f627cfcdc43a754c38 100644
--- a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
+++ b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
@@ -38,6 +38,7 @@ import io.xxlabs.messenger.repository.DaoRepository
 import io.xxlabs.messenger.repository.PreferencesRepository
 import io.xxlabs.messenger.repository.base.BasePreferences
 import io.xxlabs.messenger.repository.base.BaseRepository
+import io.xxlabs.messenger.repository.client.NodeErrorException.Companion.isNodeError
 import io.xxlabs.messenger.support.appContext
 import io.xxlabs.messenger.support.extensions.fromBase64toByteArray
 import io.xxlabs.messenger.support.extensions.toBase64String
@@ -953,7 +954,13 @@ class ClientRepository @Inject constructor(
     override fun areNodesReady(): Boolean = recursiveAreNodesReady()
 
     private fun recursiveAreNodesReady(retries: Int = 0): Boolean {
-        val status = clientWrapper.getNodeRegistrationStatus()
+        val status = try {
+             clientWrapper.getNodeRegistrationStatus()
+        } catch (e: Exception) {
+            if (e.isNodeError()) return false
+            else throw e
+        }
+
         val rate: Double = ((status.first.toDouble() / status.second))
         Timber.v("[NODE REGISTRATION STATUS] Registration rate: $rate")
 
@@ -1054,4 +1061,10 @@ class ClientRepository @Inject constructor(
 }
 
 
-class NodeErrorException : Exception()
\ No newline at end of file
+class NodeErrorException : Exception() {
+    companion object {
+        fun Exception.isNodeError(): Boolean =
+            message?.contains("network is not healthy", false)
+                ?: false
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt
index 2786bc8e249e444f983fd008ae1ba47287cbb90c..5f5cd42142c5552586d305ad5461fc1f3a361d06 100644
--- a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt
+++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsFragment.kt
@@ -48,16 +48,15 @@ import kotlinx.coroutines.launch
 import java.lang.Exception
 import javax.inject.Inject
 
-class RequestsFragment : BaseFragment() {
+open class RequestsFragment : BaseFragment() {
 
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
-    private val requestsViewModel: RequestsViewModel by viewModels { viewModelFactory }
+    protected val requestsViewModel: RequestsViewModel by viewModels { viewModelFactory }
 
-    private lateinit var navController: NavController
-    private lateinit var stateAdapter: ViewPagerFragmentStateAdapter
-
-    private lateinit var toastHandler : CustomToastActivity
+    protected open val navController: NavController by lazy { findNavController() }
+    protected lateinit var stateAdapter: ViewPagerFragmentStateAdapter
+    protected lateinit var toastHandler : CustomToastActivity
 
     override fun onAttach(context: Context) {
         super.onAttach(context)
@@ -78,7 +77,7 @@ class RequestsFragment : BaseFragment() {
         return inflater.inflate(R.layout.fragment_requests, container, false)
     }
 
-    private fun observeUI() {
+    protected open fun observeUI() {
         requestsViewModel.showReceivedRequestDetails.onEach { request ->
             request?.let {
                 safelyInvoke { showRequestDetails(request) }
@@ -140,24 +139,25 @@ class RequestsFragment : BaseFragment() {
     /**
      * Prevents crash caused by user closing/navigating away when a dialog is about to display.
      */
-    private fun safelyInvoke(block: () -> Unit) {
-        if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+    protected open fun safelyInvoke(block: () -> Unit) {
+        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
             block.invoke()
         }
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        navController = findNavController()
-
-        initComponents(view)
+        initToolbar()
+        initViewPager(view)
     }
 
-    fun initComponents(root: View) {
+    protected open fun initToolbar() {
         toolbarGeneric.setInsets(topMask = WindowInsetsCompat.Type.systemBars())
         toolbarGenericTitle.text = "Requests"
         bindListeners()
+    }
 
+    protected open fun initViewPager(root: View) {
         root.apply {
             setupViewPager(requestsViewPager)
             TabLayoutMediator(requestsAppBarTabs, requestsViewPager) { tab, position ->
@@ -180,7 +180,7 @@ class RequestsFragment : BaseFragment() {
         }
     }
 
-    private fun setupViewPager(viewPager: ViewPager2) {
+    protected open fun setupViewPager(viewPager: ViewPager2) {
         stateAdapter = ViewPagerFragmentStateAdapter(childFragmentManager, lifecycle)
         stateAdapter.addFragment(
             ReceivedRequestsFragment(),
@@ -205,7 +205,7 @@ class RequestsFragment : BaseFragment() {
         viewPager.setCurrentItem(selectedTab, false)
     }
 
-    private fun getTabIcon(resourceId: Int): Drawable? {
+    protected fun getTabIcon(resourceId: Int): Drawable? {
         return try {
             ResourcesCompat.getDrawable(resources, resourceId, null)
         } catch (e: Exception) {
@@ -244,7 +244,7 @@ class RequestsFragment : BaseFragment() {
             .show(childFragmentManager, null)
     }
 
-    private fun showCustomToast(ui: ToastUI) {
+    protected fun showCustomToast(ui: ToastUI) {
         toastHandler.showCustomToast(ui)
     }
 
diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt
index 6dc8f07e4d12284f15ab2f57834968030767d469..c3234c566f0b253c87aaded741c178d48d333a85 100644
--- a/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt
+++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/RequestsViewModel.kt
@@ -89,6 +89,9 @@ class RequestsViewModel @Inject constructor(
     val sendContactRequest: StateFlow<ContactData?> by ::_sendContactRequest
     private val _sendContactRequest = MutableStateFlow<ContactData?>(null)
 
+    val showContactRequestDialog: LiveData<ContactData?> by ::_showContactRequestDialog
+    private val _showContactRequestDialog = MutableLiveData<ContactData?>(null)
+
     val showCreateNickname: StateFlow<OutgoingRequest?> by ::_showCreateNickname
     private val _showCreateNickname = MutableStateFlow<OutgoingRequest?>(null)
 
@@ -344,12 +347,28 @@ class RequestsViewModel @Inject constructor(
         when (request.request.requestStatus) {
             VERIFYING -> showVerifyingInfo()
             VERIFIED, HIDDEN -> showDetails(request)
+            ACCEPTED -> {
+                (request.request as? ContactRequest)?.model?.let { contact ->
+                    sendMessage(contact)
+                }
+            }
+            SEARCH -> {
+                (request.request as? ContactRequest)?.model?.let { user ->
+                    _showContactRequestDialog.value = user as ContactData
+                }
+            }
         }
     }
 
+    fun onSendRequestDialogShown() {
+        _showContactRequestDialog.value = null
+    }
+
     private fun showDetails(item: RequestItem) {
         when (item) {
+            is ContactRequestSearchResultItem -> showRequestDialog(item.contactRequest)
             is ContactRequestItem -> showRequestDialog(item.contactRequest)
+            is SearchResultItem -> showRequestDialog(item.contactRequest)
             is GroupInviteItem -> showInvitationDialog(item.invite)
         }
     }
@@ -414,6 +433,7 @@ class RequestsViewModel @Inject constructor(
     private fun resendRequest(item: RequestItem) {
         when (item) {
             is ContactRequestItem -> requestsDataSource.send(item.request as ContactRequest)
+            is ContactRequestSearchResultItem -> requestsDataSource.send(item.request as ContactRequest)
             is GroupInviteItem -> invitationsDataSource.send(item.request as GroupInvitation)
         }
         onResend(item)
diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt
index cee801a26b8ac7020688404802d2642be9c7a9d7..62f2f5d5a804ab8d2afeac41a0f741ecbf642ae5 100644
--- a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt
+++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItem.kt
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
 import androidx.annotation.IdRes
 import io.xxlabs.messenger.R
 import io.xxlabs.messenger.data.datatype.RequestStatus.*
+import io.xxlabs.messenger.data.room.model.ContactData
 import io.xxlabs.messenger.data.room.model.formattedEmail
 import io.xxlabs.messenger.data.room.model.formattedPhone
 import io.xxlabs.messenger.requests.model.ContactRequest
@@ -20,7 +21,7 @@ sealed class RequestItem(val request: Request) : ItemThumbnail {
     abstract val subtitle: String?
     abstract val details: String?
 
-    val actionLabel: String? =
+    open val actionLabel: String? =
         when (request.requestStatus) {
             VERIFYING -> appContext().getString(R.string.request_item_action_verifying)
             VERIFICATION_FAIL -> appContext().getString(R.string.request_item_action_failed_verification)
@@ -29,7 +30,7 @@ sealed class RequestItem(val request: Request) : ItemThumbnail {
             else -> null
         }
 
-    val actionIcon: Int? =
+    open val actionIcon: Int? =
         when (request.requestStatus) {
             VERIFICATION_FAIL -> R.drawable.ic_info_outline_24dp
             SEND_FAIL, SENT -> R.drawable.ic_retry
@@ -37,7 +38,7 @@ sealed class RequestItem(val request: Request) : ItemThumbnail {
             else -> null
         }
 
-    val actionIconColor: Int? =
+    open val actionIconColor: Int? =
         when (request.requestStatus) {
             VERIFICATION_FAIL -> R.color.accent_danger
             SEND_FAIL, SENT-> R.color.brand_default
@@ -46,7 +47,7 @@ sealed class RequestItem(val request: Request) : ItemThumbnail {
         }
 
     @IdRes
-    val actionTextStyle: Int? =
+    open val actionTextStyle: Int? =
         when (request.requestStatus) {
             VERIFYING -> R.style.request_item_verifying
             VERIFICATION_FAIL -> R.style.request_item_error
@@ -58,21 +59,39 @@ sealed class RequestItem(val request: Request) : ItemThumbnail {
 
 data class ContactRequestItem(
     val contactRequest: ContactRequest,
-    val photo: Bitmap?,
+    val photo: Bitmap? = null,
 ) : RequestItem(contactRequest) {
     override val subtitle: String? = null
     override val details: String? = contactRequest.getContactInfo()
     override val itemPhoto: Bitmap? = photo
     override val itemInitials: String = contactRequest.model.initials
     override val itemIconRes: Int? = null
+}
 
-    private fun ContactRequest.getContactInfo(): String? =
-        with ("${model.formattedEmail() ?: ""}\n${model.formattedPhone() ?: ""}") {
-            when {
-                isNullOrBlank() -> null
-                else -> trim()
-            }
+private fun ContactRequest.getContactInfo(): String? =
+    with ("${model.formattedEmail() ?: ""}\n${model.formattedPhone() ?: ""}") {
+        when {
+            isNullOrBlank() -> null
+            else -> trim()
         }
+    }
+
+data class ContactRequestSearchResultItem(
+    val contactRequest: ContactRequest,
+    val photo: Bitmap? = null,
+    val statusText: String = "Request pending",
+    val statusTextColor: Int = R.color.neutral_weak,
+    val actionVisible: Boolean = true,
+    override val actionIcon: Int = R.drawable.ic_retry,
+    override val actionIconColor: Int = R.color.brand_default,
+    override val actionTextStyle: Int = R.style.request_item_retry,
+    override val actionLabel: String = if (actionVisible) appContext().getString(R.string.request_item_action_retry) else ""
+) : RequestItem(contactRequest) {
+    override val subtitle: String = statusText
+    override val details: String? = null
+    override val itemPhoto: Bitmap? = photo
+    override val itemInitials: String = contactRequest.model.initials
+    override val itemIconRes: Int? = null
 }
 
 data class GroupInviteItem(
@@ -107,4 +126,38 @@ data class HiddenRequestToggleItem(
     override val itemInitials: String? = null
     override val subtitle: String? = null
     override val details: String? = null
+}
+
+data class AcceptedConnectionItem(
+    val contactRequest: ContactRequest,
+    val photo: Bitmap? = null
+) : RequestItem(contactRequest) {
+    override val subtitle: String? = null
+    override val details: String? = contactRequest.getContactInfo()
+    override val itemPhoto: Bitmap? = photo
+    override val itemInitials: String = contactRequest.model.initials
+    override val itemIconRes: Int? = null
+}
+
+data class SearchResultItem(
+    val contactRequest: ContactRequest,
+    val photo: Bitmap? = null
+) : RequestItem(contactRequest) {
+    override val subtitle: String? = null
+    override val details: String? = contactRequest.getContactInfo()
+    override val itemPhoto: Bitmap? = photo
+    override val itemInitials: String = contactRequest.model.initials
+    override val itemIconRes: Int? = null
+}
+
+data class ConnectionsDividerItem(
+    val placeholder: NullRequest = NullRequest(),
+    val text: String = "Local results"
+) : RequestItem(placeholder) {
+    override val title = text
+    override val itemPhoto: Bitmap? = null
+    override val itemIconRes: Int? = null
+    override val itemInitials: String? = null
+    override val subtitle: String? = null
+    override val details: String? = null
 }
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt
index aded82f79dacdcba9ff4328b3abf276efe3e8ba8..c6fcb9191c8692f73adb18d2ac220236f29b98bc 100644
--- a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt
+++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestItemViewHolder.kt
@@ -9,6 +9,8 @@ import io.xxlabs.messenger.R
 import io.xxlabs.messenger.databinding.ListItemEmptyPlaceholderBinding
 import io.xxlabs.messenger.databinding.ListItemHiddenRequestsToggleBinding
 import io.xxlabs.messenger.databinding.ListItemRequestBinding
+import io.xxlabs.messenger.databinding.ListItemRequestSearchResultBinding
+import io.xxlabs.messenger.databinding.ListItemSectionDividerBinding
 import timber.log.Timber
 import java.io.InvalidObjectException
 
@@ -23,6 +25,7 @@ class RequestViewHolder(
     override fun onBind(ui: RequestItem, listener: RequestItemListener) {
         binding.ui = ui
         binding.listener = listener
+        listener.markAsSeen(ui)
     }
 
     companion object {
@@ -37,6 +40,53 @@ class RequestViewHolder(
     }
 }
 
+/**
+ * For Connection Requests being shown as search results on the Search screen.
+ */
+class RequestSearchResultViewHolder(
+    private val binding: ListItemRequestSearchResultBinding
+) : RequestItemViewHolder(binding.root) {
+
+    override fun onBind(ui: RequestItem, listener: RequestItemListener) {
+        binding.ui = ui as ContactRequestSearchResultItem
+        binding.listener = listener
+        listener.markAsSeen(ui)
+    }
+
+    companion object {
+        fun create(parent: ViewGroup): RequestSearchResultViewHolder {
+            val binding = ListItemRequestSearchResultBinding.inflate(
+                LayoutInflater.from(parent.context),
+                parent,
+                false
+            )
+            return RequestSearchResultViewHolder(binding)
+        }
+    }
+}
+
+class ConnectionViewHolder(
+    private val binding: ListItemRequestBinding
+) : RequestItemViewHolder(binding.root) {
+
+    override fun onBind(ui: RequestItem, listener: RequestItemListener) {
+        binding.ui = ui
+        binding.listener = listener
+        binding.requestTimestamp.visibility = View.GONE
+    }
+
+    companion object {
+        fun create(parent: ViewGroup): ConnectionViewHolder {
+            val binding = ListItemRequestBinding.inflate(
+                LayoutInflater.from(parent.context),
+                parent,
+                false
+            )
+            return ConnectionViewHolder(binding)
+        }
+    }
+}
+
 class Placeholder(
     private val binding: ListItemEmptyPlaceholderBinding
 ) : RequestItemViewHolder(binding.root) {
@@ -77,6 +127,26 @@ class HiddenRequestToggle(
     }
 }
 
+class ConnectionsSectionDivider(
+    private val binding: ListItemSectionDividerBinding
+) : RequestItemViewHolder(binding.root) {
+
+    override fun onBind(ui: RequestItem, listener: RequestItemListener) {
+        binding.ui = ui
+    }
+
+    companion object {
+        fun create(parent: ViewGroup): ConnectionsSectionDivider {
+            val binding = ListItemSectionDividerBinding.inflate(
+                LayoutInflater.from(parent.context),
+                parent,
+                false
+            )
+            return ConnectionsSectionDivider(binding)
+        }
+    }
+}
+
 /**
  * Displays an invisible ViewHolder and logs the event to prevent
  * locking the screen in a permanent crash state.
diff --git a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt
index 237fbe265f672d0f7544bfb621165ad725a2ab40..106b94c558e11ff24bbd76d41b79c4a7cfbd73da 100644
--- a/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt
+++ b/app/src/main/java/io/xxlabs/messenger/requests/ui/list/adapter/RequestsAdapter.kt
@@ -4,6 +4,7 @@ import android.view.ViewGroup
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.ListAdapter
 import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter.ViewType.*
+import io.xxlabs.messenger.support.extensions.toBase64String
 
 class RequestsAdapter(
     private val listener: RequestItemListener
@@ -11,9 +12,12 @@ class RequestsAdapter(
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RequestItemViewHolder {
         return when (ViewType.from(viewType)) {
+            CONNECTION, UD_SEARCH_RESULT -> ConnectionViewHolder.create(parent)
             REQUEST, INVITE -> RequestViewHolder.create(parent)
+            REQUEST_SEARCH_RESULT -> RequestSearchResultViewHolder.create(parent)
             PLACEHOLDER -> Placeholder.create(parent)
             SWITCH -> HiddenRequestToggle.create(parent)
+            DIVIDER -> ConnectionsSectionDivider.create(parent)
             OTHER -> InvalidViewType.create(parent)
         }
     }
@@ -21,7 +25,6 @@ class RequestsAdapter(
     override fun onBindViewHolder(holder: RequestItemViewHolder, position: Int) {
         with(currentList[position]) {
             holder.onBind(this, listener)
-            listener.markAsSeen(this)
         }
     }
 
@@ -33,6 +36,10 @@ class RequestsAdapter(
                 is GroupInviteItem -> INVITE.value
                 is EmptyPlaceholderItem -> PLACEHOLDER.value
                 is HiddenRequestToggleItem -> SWITCH.value
+                is AcceptedConnectionItem -> CONNECTION.value
+                is SearchResultItem -> UD_SEARCH_RESULT.value
+                is ConnectionsDividerItem -> DIVIDER.value
+                is ContactRequestSearchResultItem -> REQUEST_SEARCH_RESULT.value
                 else -> OTHER.value
             }
             status + model
@@ -44,7 +51,11 @@ class RequestsAdapter(
         INVITE(200),
         PLACEHOLDER(300),
         SWITCH(400),
-        OTHER(500);
+        CONNECTION(500),
+        UD_SEARCH_RESULT(600),
+        DIVIDER(700),
+        REQUEST_SEARCH_RESULT(800),
+        OTHER(900);
 
         companion object {
             fun from(value: Int): ViewType {
@@ -58,7 +69,7 @@ class RequestsAdapter(
 
 class RequestsDiffCallback : DiffUtil.ItemCallback<RequestItem>() {
     override fun areItemsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean =
-        oldItem.id.contentEquals(newItem.id)
+        oldItem.id.toBase64String() == newItem.id.toBase64String()
 
     override fun areContentsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean =
         oldItem == newItem
diff --git a/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5b5e87be38e3cdadf3c5a8f8dc16d068b0f5f459
--- /dev/null
+++ b/app/src/main/java/io/xxlabs/messenger/search/FactSearchFragment.kt
@@ -0,0 +1,166 @@
+package io.xxlabs.messenger.search
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.SpannableString
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.textfield.TextInputEditText
+import io.xxlabs.messenger.databinding.FragmentFactSearchBinding
+import io.xxlabs.messenger.di.utils.Injectable
+import io.xxlabs.messenger.requests.ui.RequestsViewModel
+import io.xxlabs.messenger.requests.ui.list.adapter.RequestItem
+import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * Superclass for Fragments that look up users by Fact (username, phone, etc.)
+ */
+abstract class FactSearchFragment : Fragment(), Injectable {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+    protected val requestsViewModel: RequestsViewModel by viewModels(
+        ownerProducer = { requireParentFragment() },
+        factoryProducer = { viewModelFactory }
+    )
+    protected val searchViewModel: UserSearchViewModel by viewModels(
+        ownerProducer = { requireParentFragment() },
+        factoryProducer = { viewModelFactory }
+    )
+
+    protected val resultsAdapter: RequestsAdapter by lazy {
+        RequestsAdapter(requestsViewModel)
+    }
+    protected lateinit var binding: FragmentFactSearchBinding
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = FragmentFactSearchBinding.inflate(inflater, container, false)
+        binding.lifecycleOwner = viewLifecycleOwner
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initComponents()
+    }
+
+    private fun initComponents() {
+        binding.searchResultsRV.apply {
+            layoutManager = LinearLayoutManager(requireContext())
+            adapter = resultsAdapter
+        }
+        binding.searchTextInputEditText.apply {
+            initKeyboardSearchButton()
+            initFocusListener()
+        }
+        binding.ui = getSearchTabUi()
+    }
+
+    private fun TextInputEditText.initKeyboardSearchButton() {
+        setOnEditorActionListener { v, actionId, _ ->
+            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+                try {
+                    onSearchClicked(v.text.toString())
+                    return@setOnEditorActionListener true
+                } catch (e: Exception) {
+                    return@setOnEditorActionListener false
+                }
+
+            } else {
+                return@setOnEditorActionListener false
+            }
+        }
+    }
+
+    private fun TextInputEditText.initFocusListener() {
+        setOnFocusChangeListener { view, hasFocus ->
+            if (hasFocus) searchViewModel.onUserInput("")
+        }
+    }
+
+    abstract suspend fun getResults(): Flow<List<RequestItem>>
+
+    abstract fun onSearchClicked(query: String?)
+
+    abstract fun getSearchTabUi(): FactSearchUi
+}
+
+class UsernameSearchFragment : FactSearchFragment() {
+    override suspend fun getResults(): Flow<List<RequestItem>> =
+        searchViewModel.usernameResults
+
+    override fun onSearchClicked(query: String?) {
+        lifecycleScope.launch {
+            searchViewModel.onUsernameSearch(query).collect { results ->
+                resultsAdapter.submitList(results)
+            }
+        }
+    }
+
+    override fun getSearchTabUi(): FactSearchUi = searchViewModel.usernameSearchUi
+
+    override fun onResume() {
+        super.onResume()
+        searchViewModel.invitationFrom.observe(viewLifecycleOwner) { username ->
+            username?.let {
+                binding.searchTextInputEditText.setText(username)
+                onSearchClicked(username)
+                searchViewModel.onInvitationHandled()
+            }
+        }
+    }
+}
+
+class EmailSearchFragment : FactSearchFragment() {
+    override suspend fun getResults(): Flow<List<RequestItem>> =
+        searchViewModel.emailResults
+
+    override fun onSearchClicked(query: String?) {
+        lifecycleScope.launch {
+            searchViewModel.onEmailSearch(query).collect { results ->
+                resultsAdapter.submitList(results)
+            }
+        }
+    }
+
+    override fun getSearchTabUi(): FactSearchUi = searchViewModel.emailSearchUi
+}
+
+class PhoneSearchFragment : FactSearchFragment() {
+    override suspend fun getResults(): Flow<List<RequestItem>> =
+        searchViewModel.phoneResults
+
+    override fun onSearchClicked(query: String?) {
+        lifecycleScope.launch {
+            searchViewModel.onPhoneSearch(query).collect { results ->
+                resultsAdapter.submitList(results)
+            }
+        }
+    }
+
+    override fun getSearchTabUi(): FactSearchUi = searchViewModel.phoneSearchUi
+}
+
+class QrSearchFragment : FactSearchFragment() {
+    override suspend fun getResults(): Flow<List<RequestItem>> = flowOf()
+    override fun onSearchClicked(query: String?) {}
+    override fun getSearchTabUi(): FactSearchUi = searchViewModel.qrSearchUi
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/search/FactSearchUi.kt b/app/src/main/java/io/xxlabs/messenger/search/FactSearchUi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2de53bd887e6b16eb0dc5a1b2e9c09da06607093
--- /dev/null
+++ b/app/src/main/java/io/xxlabs/messenger/search/FactSearchUi.kt
@@ -0,0 +1,13 @@
+package io.xxlabs.messenger.search
+
+import android.text.Editable
+import androidx.lifecycle.LiveData
+
+interface FactSearchUi {
+    val countryCode: LiveData<String?>
+    val searchHint: String
+    val userInputEnabled: LiveData<Boolean>
+
+    fun onCountryClicked()
+    fun onSearchInput(editable: Editable?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/search/UdSearchUi.kt b/app/src/main/java/io/xxlabs/messenger/search/UdSearchUi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..89dac3e5af32683db800fda1ddd9c66870e4d6bb
--- /dev/null
+++ b/app/src/main/java/io/xxlabs/messenger/search/UdSearchUi.kt
@@ -0,0 +1,25 @@
+package io.xxlabs.messenger.search
+
+import android.text.Spanned
+
+interface UdSearchUi {
+    val callToActionText: Spanned?
+    val placeholderText: Spanned?
+    val placeholderVisible: Boolean
+    val isSearching: Boolean
+
+    fun onPlaceholderClicked()
+    fun onCancelSearchClicked()
+}
+
+data class SearchUiState(
+    override val callToActionText: Spanned? = null,
+    override val placeholderText: Spanned? = null,
+    override val placeholderVisible: Boolean = false,
+    override val isSearching: Boolean = false,
+    private val placeHolderClicked: () -> Unit = {},
+    private val cancelClicked: () -> Unit = {}
+): UdSearchUi {
+    override fun onPlaceholderClicked() = placeHolderClicked()
+    override fun onCancelSearchClicked() = cancelClicked()
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..02beb4e7d823fdc168eda107d2d3b3edbaa37e69
--- /dev/null
+++ b/app/src/main/java/io/xxlabs/messenger/search/UserSearchFragment.kt
@@ -0,0 +1,302 @@
+package io.xxlabs.messenger.search
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
+import com.google.android.material.tabs.TabLayoutMediator
+import io.xxlabs.messenger.R
+import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase
+import io.xxlabs.messenger.data.room.model.ContactData
+import io.xxlabs.messenger.databinding.FragmentUserSearchBinding
+import io.xxlabs.messenger.requests.ui.RequestsFragment
+import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameDialog
+import io.xxlabs.messenger.requests.ui.send.OutgoingRequest
+import io.xxlabs.messenger.requests.ui.send.SendRequestDialog
+import io.xxlabs.messenger.support.extensions.setInsets
+import io.xxlabs.messenger.support.toast.CustomToastActivity
+import io.xxlabs.messenger.support.toast.ToastUI
+import io.xxlabs.messenger.ui.base.ViewPagerFragmentStateAdapter
+import io.xxlabs.messenger.ui.dialog.info.InfoDialog
+import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI
+import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialog
+import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI
+import io.xxlabs.messenger.ui.global.ContactsViewModel
+import io.xxlabs.messenger.ui.main.countrycode.CountryFullscreenDialog
+import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener
+import kotlinx.android.synthetic.main.component_toolbar_generic.*
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.lang.Exception
+
+class UserSearchFragment : RequestsFragment() {
+
+    private val contactsViewModel: ContactsViewModel by viewModels { viewModelFactory }
+    private val searchViewModel: UserSearchViewModel by viewModels { viewModelFactory }
+
+    private lateinit var binding: FragmentUserSearchBinding
+    override val navController: NavController by lazy {
+        findNavController()
+    }
+    private val invitationUsername: String? by lazy {
+        UserSearchFragmentArgs.fromBundle(requireArguments()).username
+    }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        (context as? CustomToastActivity)?.run {
+            toastHandler = this
+        } ?: throw Exception("Activity must implement CustomToastActivity!")
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                observeUI()
+            }
+        }
+        binding = FragmentUserSearchBinding.inflate(
+            inflater, container, false
+        )
+        binding.lifecycleOwner = viewLifecycleOwner
+        return binding.root
+    }
+
+    override fun initToolbar() {
+        binding.userSearchAppBarLayout.apply {
+            toolbarGeneric.setInsets(topMask = WindowInsetsCompat.Type.systemBars())
+            toolbarGenericActionText.visibility = View.VISIBLE
+            toolbarGenericTitle.text = requireContext().getString(R.string.search_title)
+            toolbarGenericBackBtn.setOnClickListener { navController.navigateUp() }
+        }
+    }
+
+    override fun initViewPager(root: View) {
+        binding.apply {
+            setupViewPager(userSearchViewPager)
+            TabLayoutMediator(userSearchAppBarTabs, userSearchViewPager) { tab, position ->
+                tab.apply {
+                    text = stateAdapter.getPageTitle(position)
+                    icon = stateAdapter.getIcon(position)
+                    contentDescription = when (position) {
+                        SEARCH_USERNAME -> "search.tab.username"
+                        SEARCH_EMAIL -> "search.tab.email"
+                        SEARCH_PHONE -> "search.tab.phone"
+                        SEARCH_QR -> "search.tab.qr"
+                        else -> "search.tab.invalid"
+                    }
+                }
+            }.attach()
+
+            userSearchAppBarTabs.addOnTabSelectedListener(
+                object : OnTabSelectedListener {
+                    override fun onTabSelected(tab: TabLayout.Tab?) {
+                        if (searchViewModel.previousTabPosition != SEARCH_QR && tab?.position == SEARCH_QR) {
+                            searchViewModel.previousTabPosition = SEARCH_QR
+                            lifecycleScope.launch {
+                                delay(500)
+                                navigateToQrCode()
+                            }
+                        } else {
+                            searchViewModel.previousTabPosition = tab?.position ?: 0
+                        }
+                    }
+
+                    override fun onTabUnselected(tab: TabLayout.Tab?) { }
+                    override fun onTabReselected(tab: TabLayout.Tab?) { }
+                }
+            )
+        }
+    }
+
+    private fun navigateToQrCode() {
+        val qrCodeScreen = UserSearchFragmentDirections.actionGlobalQrCode()
+        findNavController().navigate(qrCodeScreen)
+    }
+
+    override fun setupViewPager(viewPager: ViewPager2) {
+        stateAdapter = ViewPagerFragmentStateAdapter(childFragmentManager, lifecycle)
+        stateAdapter.addFragment(
+            UsernameSearchFragment(),
+            "Username",
+            getTabIcon(R.drawable.ic_contact_light)
+        )
+        stateAdapter.addFragment(
+            EmailSearchFragment(),
+            "Email",
+            getTabIcon(R.drawable.ic_mail)
+        )
+        stateAdapter.addFragment(
+            PhoneSearchFragment(),
+            "Phone",
+            getTabIcon(R.drawable.ic_phone)
+        )
+        stateAdapter.addFragment(
+            QrSearchFragment(),
+            "QR Code",
+            getTabIcon(R.drawable.ic_qr_code_label)
+        )
+
+        viewPager.adapter = stateAdapter
+        viewPager.offscreenPageLimit = 3
+
+        val selectedTab = arguments?.getInt("selectedTab") ?: 0
+        viewPager.setCurrentItem(selectedTab, false)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        handleInvitation()
+        observeUi()
+    }
+
+    private fun handleInvitation() {
+        invitationUsername?.let {
+            searchViewModel.onInvitationReceived(it)
+        }
+    }
+
+    private fun observeUi() {
+        searchViewModel.udSearchUi.observe(viewLifecycleOwner) { state ->
+            binding.ui = state
+        }
+
+        super.observeUI()
+
+        requestsViewModel.sendContactRequest.onEach { toUser ->
+            toUser?.let {
+                contactsViewModel.updateAndRequestAuthChannel(toUser)
+                requestsViewModel.onSendRequestHandled()
+            }
+        }.launchIn(lifecycleScope)
+
+        requestsViewModel.showCreateNickname.onEach { outgoingRequest ->
+            outgoingRequest?.let {
+                showSaveNicknameDialog(it)
+                requestsViewModel.onShowCreateNicknameHandled()
+            }
+        }.launchIn(lifecycleScope)
+
+        searchViewModel.dialogUi.observe(viewLifecycleOwner) { ui ->
+            ui?.let {
+                showDialog(ui)
+                searchViewModel.onDialogShown()
+            }
+        }
+
+        searchViewModel.toastUi.observe(viewLifecycleOwner) { ui ->
+            ui?.let {
+                showToast(ui)
+                searchViewModel.onToastShown()
+            }
+        }
+
+        searchViewModel.searchInfoDialog.observe(viewLifecycleOwner) { ui ->
+            ui?.let {
+                showDialog(ui)
+                searchViewModel.onInfoDialogShown()
+            }
+        }
+
+        searchViewModel.selectCountry.observe(viewLifecycleOwner) { listener ->
+            listener?.let {
+                selectCountry(it)
+            }
+        }
+
+        searchViewModel.dismissCountries.observe(viewLifecycleOwner) { dismiss ->
+            if (dismiss) {
+                dismissCountryList()
+                searchViewModel.onCountriesDismissed()
+            }
+        }
+
+        requestsViewModel.showContactRequestDialog.observe(viewLifecycleOwner) { user ->
+            user?.let {
+                showSendRequestDialog(it)
+                requestsViewModel.onSendRequestDialogShown()
+            }
+        }
+    }
+
+    private var countryList: CountryFullscreenDialog? = null
+
+    private fun selectCountry(listener: CountrySelectionListener) {
+        safelyInvoke {
+            countryList = CountryFullscreenDialog.getInstance(listener)
+            countryList?.show(parentFragmentManager, null)
+        }
+    }
+
+    private fun dismissCountryList() {
+        try {
+            countryList?.dismiss()
+            countryList = null
+        } catch (e: Exception) {
+            Timber.d("Exception occured when dismissing countries list: ${e.message}")
+        }
+    }
+
+    private fun showToast(ui: ToastUI) {
+        safelyInvoke {
+            toastHandler.showCustomToast(ui)
+        }
+    }
+
+    private fun showDialog(ui: TwoButtonInfoDialogUI) {
+        safelyInvoke {
+            TwoButtonInfoDialog
+                .newInstance(ui)
+                .show(parentFragmentManager, null)
+        }
+    }
+
+    private fun showDialog(ui: InfoDialogUI) {
+        safelyInvoke {
+            InfoDialog
+                .newInstance(ui)
+                .show(parentFragmentManager, null)
+        }
+    }
+
+    private fun showSaveNicknameDialog(outgoingRequest: OutgoingRequest) {
+        safelyInvoke {
+            SaveNicknameDialog
+                .newInstance(outgoingRequest)
+                .show(childFragmentManager, null)
+        }
+    }
+
+    private fun showSendRequestDialog(user: ContactData) {
+        safelyInvoke {
+            SendRequestDialog
+                .newInstance(user)
+                .show(childFragmentManager, null)
+        }
+    }
+
+    companion object {
+        const val SEARCH_USERNAME = 0
+        const val SEARCH_EMAIL = 1
+        const val SEARCH_PHONE = 2
+        const val SEARCH_QR = 3
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt b/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..47947cb6e230838a98817ee1c3c4e369311a361a
--- /dev/null
+++ b/app/src/main/java/io/xxlabs/messenger/search/UserSearchViewModel.kt
@@ -0,0 +1,724 @@
+package io.xxlabs.messenger.search
+
+import android.graphics.Bitmap
+import android.text.Editable
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import androidx.lifecycle.*
+import io.xxlabs.messenger.R
+import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase
+import io.xxlabs.messenger.data.data.Country
+import io.xxlabs.messenger.data.datatype.FactType
+import io.xxlabs.messenger.data.datatype.RequestStatus
+import io.xxlabs.messenger.data.room.model.Contact
+import io.xxlabs.messenger.data.room.model.ContactData
+import io.xxlabs.messenger.repository.DaoRepository
+import io.xxlabs.messenger.repository.PreferencesRepository
+import io.xxlabs.messenger.repository.base.BaseRepository
+import io.xxlabs.messenger.repository.client.NodeErrorException
+import io.xxlabs.messenger.requests.data.contact.ContactRequestData
+import io.xxlabs.messenger.requests.data.contact.ContactRequestsRepository
+import io.xxlabs.messenger.requests.model.ContactRequest
+import io.xxlabs.messenger.requests.ui.list.adapter.*
+import io.xxlabs.messenger.support.appContext
+import io.xxlabs.messenger.support.toast.ToastUI
+import io.xxlabs.messenger.support.util.value
+import io.xxlabs.messenger.support.view.BitmapResolver
+import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI
+import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI
+import io.xxlabs.messenger.ui.dialog.info.createInfoDialog
+import io.xxlabs.messenger.ui.dialog.info.createTwoButtonDialogUi
+import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.*
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.coroutines.coroutineContext
+
+class UserSearchViewModel @Inject constructor(
+    private val repo: BaseRepository,
+    private val daoRepo: DaoRepository,
+    private val preferences: PreferencesRepository,
+    private val requestsDataSource: ContactRequestsRepository,
+): ViewModel(){
+
+    private val genericSearchError: String by lazy {
+        appContext().getString(R.string.search_generic_error_message)
+    }
+
+    var previousTabPosition: Int = UserSearchFragment.SEARCH_USERNAME
+
+    private val initialState: SearchUiState by lazy {
+        SearchUiState(
+            callToActionText = callToActionText,
+            placeholderText = placeholderText,
+            placeholderVisible = true,
+            placeHolderClicked = ::onPlaceholderClicked
+        )
+    }
+
+    private val userInputState: SearchUiState by lazy {
+        SearchUiState()
+    }
+
+    private val searchRunningState: SearchUiState by lazy {
+        SearchUiState(
+            isSearching = true,
+            cancelClicked = ::onCancelSearchClicked
+        )
+    }
+
+    private val searchCompleteState: SearchUiState by lazy {
+        SearchUiState(isSearching = false)
+    }
+
+    private val callToActionText: Spanned by lazy {
+        val highlight = appContext().getColor(R.color.brand_default)
+        val cta = appContext().getString(R.string.search_call_to_action)
+
+        val span1 = appContext().getString(R.string.search_call_to_action_span_1)
+        val span1Start = cta.indexOf(span1, ignoreCase = true)
+        val span1End = span1Start + span1.length
+
+        val span2 = appContext().getString(R.string.search_call_to_action_span_2)
+        val span2Start = cta.indexOf(span2, ignoreCase = true)
+        val span2End = span2Start + span2.length
+
+        SpannableString(cta).apply {
+            setSpan(
+                ForegroundColorSpan(highlight),
+                span1Start,
+                span1End,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+            setSpan(
+                ForegroundColorSpan(highlight),
+                span2Start,
+                span2End,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+        }
+    }
+
+    private val placeholderText: Spanned by lazy {
+        val highlight = appContext().getColor(R.color.brand_default)
+        val text = appContext().getString(R.string.search_placeholder_text)
+        val span = appContext().getString(R.string.search_placeholder_span)
+        val startIndex = text.indexOf(span, ignoreCase = true)
+
+        SpannableString(text).apply {
+            setSpan(
+                ForegroundColorSpan(highlight),
+                startIndex,
+                startIndex + span.length,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            )
+        }
+    }
+
+    val searchInfoDialog: LiveData<InfoDialogUI?> by ::_searchInfoDialog
+    private val _searchInfoDialog = MutableLiveData<InfoDialogUI?>(null)
+
+    private val searchInfoDialogUi: InfoDialogUI by lazy {
+        createInfoDialog(
+            title = R.string.search_info_dialog_title,
+            body = R.string.search_info_dialog_body,
+            linkTextToUrlMap = mapOf(
+                appContext().getString(R.string.search_info_dialog_link_text)
+                        to appContext().getString(R.string.search_info_dialog_link_url)
+            )
+        )
+    }
+
+    val udSearchUi: LiveData<UdSearchUi> by ::_udSearchUi
+    private val _udSearchUi = MutableLiveData<UdSearchUi>(initialState)
+
+    val dialogUi: LiveData<TwoButtonInfoDialogUI?> by ::_dialogUi
+    private val _dialogUi = MutableLiveData<TwoButtonInfoDialogUI?>(null)
+
+    val toastUi: LiveData<ToastUI?> by ::_toastUi
+    private val _toastUi = MutableLiveData<ToastUI?>(null)
+
+    private val _userInputEnabled = Transformations.map(udSearchUi) { state ->
+        state != searchRunningState
+    }
+
+    val usernameSearchUi: FactSearchUi by lazy {
+        object : FactSearchUi {
+            override val countryCode: LiveData<String?> = MutableLiveData(null)
+            override val searchHint: String = "Search by username"
+            override val userInputEnabled: LiveData<Boolean> by ::_userInputEnabled
+            override fun onCountryClicked() {}
+            override fun onSearchInput(editable: Editable?) = onUserInput(editable?.toString())
+        }
+    }
+    val emailSearchUi: FactSearchUi by lazy {
+        object : FactSearchUi {
+            override val countryCode: LiveData<String?> = MutableLiveData(null)
+            override val searchHint: String = "Search by email address"
+            override val userInputEnabled: LiveData<Boolean> by ::_userInputEnabled
+            override fun onCountryClicked() {}
+            override fun onSearchInput(editable: Editable?) = onUserInput(editable?.toString())
+        }
+    }
+    val phoneSearchUi: FactSearchUi by lazy {
+        object : FactSearchUi {
+            override val countryCode: LiveData<String?> by ::dialCode
+            override val searchHint: String = "Search by phone number"
+            override val userInputEnabled: LiveData<Boolean> by ::_userInputEnabled
+            override fun onCountryClicked() { onCountryCodeClicked() }
+            override fun onSearchInput(editable: Editable?) = onUserInput(editable?.toString())
+        }
+    }
+    val qrSearchUi: FactSearchUi by lazy {
+        object : FactSearchUi {
+            override val countryCode: LiveData<String?> = MutableLiveData(null)
+            override val searchHint: String = "Search by QR code"
+            override val userInputEnabled: LiveData<Boolean> = MutableLiveData(false)
+            override fun onCountryClicked() { }
+            override fun onSearchInput(editable: Editable?) = onUserInput(editable?.toString())
+        }
+    }
+
+    private var country: Country = Country.getDefaultCountry()
+        set(value) {
+            dialCode.value = "${value.flag}  ${value.dialCode}"
+            field = value
+        }
+    private val dialCode = MutableLiveData("${country.flag}  ${country.dialCode}")
+
+    val selectCountry: LiveData<CountrySelectionListener?> by ::_selectCountry
+    private val _selectCountry = MutableLiveData<CountrySelectionListener?>(null)
+
+    private val countryListener: CountrySelectionListener by lazy {
+        object : CountrySelectionListener {
+            override val onDismiss = { _selectCountry.value = null }
+            override fun onItemSelected(country: Country) = onCountrySelected(country)
+        }
+    }
+
+    val usernameResults: Flow<List<RequestItem>> by ::_usernameResults
+    private val _usernameResults = MutableStateFlow<List<RequestItem>>(listOf())
+
+    val emailResults: Flow<List<RequestItem>> by ::_emailResults
+    private val _emailResults = MutableStateFlow<List<RequestItem>>(listOf())
+
+    val phoneResults: Flow<List<RequestItem>> by ::_phoneResults
+    private val _phoneResults = MutableStateFlow<List<RequestItem>>(listOf())
+
+    private var searchJob: Job? = null
+
+    val invitationFrom: LiveData<String?> by ::_invitationFrom
+    private val _invitationFrom = MutableLiveData<String?>(null)
+
+    init {
+        showNewUserPopups()
+    }
+
+    private fun showNewUserPopups() {
+        if (preferences.isFirstTimeNotifications) {
+            showNotificationDialog()
+        }
+    }
+
+    private fun showNotificationDialog() {
+        _dialogUi.value = createTwoButtonDialogUi(
+            title = R.string.settings_push_notifications_dialog_title,
+            body = R.string.settings_push_notifications_dialog_body,
+            linkTextToUrlMap = null,
+            positiveClick = ::enablePushNotifications,
+            negativeClick = null,
+            onDismiss = ::showCoverMessageDialog
+        )
+        preferences.isFirstTimeNotifications = false
+    }
+
+    private fun enablePushNotifications() {
+        viewModelScope.launch {
+            try {
+                val notificationToken = enableNotifications()
+                onNotificationsEnabled(notificationToken)
+            } catch (e: Exception) {
+                showError(
+                    e.message ?: "Failed to enable notifications. Please try again in Settings."
+                )
+            }
+        }
+    }
+
+    private fun onNotificationsEnabled(token: String) {
+        token.run {
+            Timber.d("New token successfully sent! $this")
+            preferences.areNotificationsOn = true
+            preferences.currentNotificationsTokenId = this
+            preferences.notificationsTokenId = this
+        }
+    }
+
+    private suspend fun enableNotifications() = repo.registerNotificationsToken().value()
+
+    private fun showCoverMessageDialog() {
+        _dialogUi.value = createTwoButtonDialogUi(
+            title = R.string.settings_cover_traffic_title,
+            body = R.string.settings_cover_traffic_dialog_body,
+            linkTextToUrlMap = mapOf(
+                appContext().getString(R.string.settings_cover_traffic_link_text)
+                        to appContext().getString(R.string.settings_cover_traffic_link_url)
+            ),
+            positiveClick = { enableDummyTraffic(true) },
+            negativeClick = { enableDummyTraffic(false) },
+        )
+        preferences.isFirstTimeCoverMessages = false
+    }
+
+    private fun enableDummyTraffic(enabled: Boolean) {
+        preferences.isCoverTrafficOn = enabled
+        repo.enableDummyTraffic(enabled)
+    }
+
+    fun onInvitationReceived(username: String) {
+        _invitationFrom.value = username
+    }
+
+    fun onInvitationHandled() {
+        _invitationFrom.value = null
+    }
+
+    suspend fun onUsernameSearch(username: String?): Flow<List<RequestItem>> {
+        _usernameResults.value = listOf()
+        val factQuery = FactQuery.UsernameQuery(username)
+        return search(factQuery).cancellable()
+    }
+
+    suspend fun onEmailSearch(email: String?): Flow<List<RequestItem>> {
+        _emailResults.value = listOf()
+        val factQuery = FactQuery.EmailQuery(email)
+        return search(factQuery).cancellable()
+    }
+
+    suspend fun onPhoneSearch(phone: String?): Flow<List<RequestItem>> {
+        _phoneResults.value = listOf()
+        val factQuery = FactQuery.PhoneQuery(phone + country.countryCode)
+        return search(factQuery).cancellable()
+    }
+
+    private suspend fun search(factQuery: FactQuery): Flow<List<RequestItem>> {
+        // Cancel previous searches, save a reference to this one.
+        searchJob?.cancel()
+        searchJob = coroutineContext.job
+
+        if (!isValidQuery(factQuery)) return flowOf(listOf())
+        changeStateTo(searchRunningState)
+
+        return combine(
+            searchUd(factQuery),
+            allRequests(),
+            allConnections()
+        ) { ud, allRequests, allConnections ->
+            val foundRequests = allRequests.matching(factQuery).toMutableSet()
+            val foundConnections = allConnections.matching(factQuery).toMutableSet()
+
+            // Add identical request to request results, if not already there.
+            allRequests.identicalTo(ud.username)?.let {
+                foundRequests.add(it)
+            }
+
+            // Add identical connection to connection results, if not already there.
+            allConnections.identicalTo(ud.username)?.let {
+                foundConnections.add(it)
+            }
+
+            val alreadyRequested = ud.username in foundRequests.map { request ->
+                request.username
+            }
+
+            val alreadyAdded = ud.username in foundConnections.map { connection ->
+                connection.username
+            }
+
+            val nonConnections =
+                if (alreadyRequested || alreadyAdded) {
+                    // If the UD result's userID match a request's userID, only show the request
+                    foundRequests.toList().sortedBy { it.username }
+                } else {
+                    // Otherwise show both
+                    listOf(ud) + foundRequests.sortedBy { it.username }
+                }
+
+            if (nonConnections.isEmpty()) {
+                // If there's no UD result or Requests, just show the Connections with no divider.
+                foundConnections.toList().sortedBy {
+                    it.username
+                }.ifEmpty {
+                    // Show a "no results found" placeholder if there's nothing at all.
+                    noResultsFor(factQuery)
+                }
+            } else {
+                if (foundConnections.isEmpty()) {
+                    // If there's no Connections, show the UD & Request results.
+                    nonConnections
+                } else {
+                    // Or show the UD results, Requests, a divider, and finally Connections.
+                    nonConnections
+                        .plus(listOf(ConnectionsDividerItem()))
+                        .plus(foundConnections.toList().sortedBy { it.username })
+                }
+            }
+        }
+    }
+
+    private val RequestItem.username: String
+        get() = (request as? ContactRequest)?.model?.username ?: ""
+
+    private suspend fun allRequests(): Flow<List<RequestItem>> =
+        requestsDataSource.getRequests().map { requestsList ->
+            requestsList.map {
+                it.asRequestSearchResult()
+            }
+        }.stateIn(viewModelScope)
+
+    private suspend fun allConnections() = flow {
+        val connectionsList = savedUsers().filter {
+            it.isConnection()
+        }.asConnectionsSearchResult()
+
+        emit(connectionsList)
+    }.stateIn(viewModelScope)
+
+    private fun List<RequestItem>.identicalTo(username: String): RequestItem? =
+        firstOrNull { it.username == username }
+
+    private fun List<RequestItem>.matching(factQuery: FactQuery): List<RequestItem> {
+        return when (factQuery.type) {
+            FactType.USERNAME -> {
+                filter {
+                    (it.request as? ContactRequest)?.model?.displayName?.contains(
+                        factQuery.fact,
+                        true
+                    ) ?: false
+                }
+            }
+            FactType.EMAIL -> {
+                filter {
+                    (it.request as? ContactRequest)?.model?.email?.contains(
+                        factQuery.fact,
+                        true
+                    ) ?: false
+                }
+            }
+            FactType.PHONE -> {
+                filter {
+                    (it.request as? ContactRequest)?.model?.phone?.contains(
+                        factQuery.fact,
+                        true
+                    ) ?: false
+                }
+            }
+            else -> listOf()
+        }
+    }
+
+    private fun isValidQuery(factQuery: FactQuery): Boolean {
+        return with (factQuery.fact) {
+            if (isNullOrBlank()) {
+                // Prevent blank text
+                false
+            } else {
+                // Prevent users from searching (and possibly requesting) themselves.
+                this != repo.getStoredUsername()
+                        && this != repo.getStoredEmail()
+                        && this != repo.getStoredPhone()
+            }
+        }
+    }
+
+    private fun noResultsFor(factQuery: FactQuery): List<RequestItem> =
+        listOf(noResultPlaceholder(factQuery))
+
+    private fun noResultPlaceholder(factQuery: FactQuery): RequestItem =
+        EmptyPlaceholderItem(
+            text = "There are no users with that ${factQuery.type.name.lowercase()}."
+        )
+
+    private fun couldNotCompletePlaceholder(error: String): RequestItem =
+        EmptyPlaceholderItem(text = error)
+
+    private suspend fun savedUsers(): List<ContactData> =
+        daoRepo.getAllContacts().value()
+
+    private fun ContactData.isConnection(): Boolean =
+        RequestStatus.from(status) == RequestStatus.ACCEPTED
+
+    private suspend fun searchConnections(factQuery: FactQuery) = flow {
+        val results = when (factQuery.type) {
+            FactType.USERNAME -> {
+                savedUsers().filter {
+                    it.isConnection() && it.displayName.contains(factQuery.fact, true)
+                }.asConnectionsSearchResult()
+
+            }
+            FactType.EMAIL -> {
+                savedUsers().filter {
+                    it.isConnection() && it.email.contains(factQuery.fact, true)
+                }.asConnectionsSearchResult()
+
+            }
+            FactType.PHONE -> {
+                savedUsers().filter {
+                    it.isConnection() && it.phone.contains(factQuery.fact, true)
+                }.asConnectionsSearchResult()
+            }
+            else -> listOf()
+        }
+        emit(results)
+    }.stateIn(viewModelScope)
+
+    private suspend fun searchRequests(factQuery: FactQuery) =
+        when (factQuery.type) {
+            FactType.USERNAME -> {
+                filterRequests {
+                    it.model.displayName.contains(factQuery.fact, true)
+                }
+            }
+            FactType.EMAIL -> {
+                filterRequests {
+                    it.model.email.contains(factQuery.fact, true)
+                }
+            }
+            FactType.PHONE -> {
+                filterRequests {
+                    it.model.phone.contains(
+                        Country.toFormattedNumber(factQuery.fact, false) ?: factQuery.fact
+                    )
+                }
+            }
+            else -> flow { listOf<RequestItem>() }
+        }.stateIn(viewModelScope)
+
+    private suspend fun filterRequests(match: (contactRequest: ContactRequest) -> Boolean) =
+        requestsDataSource.getRequests().map { requestsList ->
+            requestsList.filter {
+                match(it)
+            }.map {
+                it.asRequestSearchResult()
+            }
+        }.flowOn(Dispatchers.IO)
+
+    private suspend fun List<ContactData>.asConnectionsSearchResult(): List<RequestItem> =
+        map {
+            val requestData = ContactRequestData(it)
+            AcceptedConnectionItem(
+                requestData,
+                resolveBitmap(it.photo)
+            )
+        }
+
+    private suspend fun ContactRequest.asRequestSearchResult(): RequestItem =
+        ContactRequestSearchResultItem(
+            contactRequest = this,
+            photo = resolveBitmap(model.photo),
+            statusText = model.statusText(),
+            statusTextColor = model.statusTextColor(),
+            actionVisible = model.actionVisible(),
+            actionIcon = model.actionIcon(),
+            actionIconColor = model.actionIconColor(),
+            actionTextStyle = model.actionTextStyle(),
+            actionLabel = model.actionLabel()
+        )
+
+    private fun Contact.statusText(): String {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.SENT,
+            RequestStatus.VERIFIED,
+            RequestStatus.RESET_SENT,
+            RequestStatus.RESENT,
+            RequestStatus.VERIFYING,
+            RequestStatus.HIDDEN,
+            RequestStatus.SENDING -> "Request pending"
+
+            RequestStatus.SEND_FAIL,
+            RequestStatus.CONFIRM_FAIL,
+            RequestStatus.VERIFICATION_FAIL,
+            RequestStatus.RESET_FAIL -> "Request failed"
+
+            else -> ""
+        }
+    }
+
+    private fun Contact.statusTextColor(): Int {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.SEND_FAIL,
+            RequestStatus.CONFIRM_FAIL,
+            RequestStatus.VERIFICATION_FAIL,
+            RequestStatus.RESET_FAIL ->  R.color.accent_danger
+
+            else -> R.color.neutral_weak
+        }
+    }
+
+    private fun Contact.actionVisible(): Boolean {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.VERIFIED, RequestStatus.VERIFYING, RequestStatus.HIDDEN -> false
+            else -> true
+        }
+    }
+
+    private fun Contact.actionIcon(): Int {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.RESENT -> R.drawable.ic_check_green
+            else -> R.drawable.ic_retry
+        }
+    }
+
+    private fun Contact.actionIconColor(): Int {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.RESENT ->  R.color.accent_success
+            else -> R.color.brand_default
+        }
+    }
+
+    private fun Contact.actionTextStyle(): Int {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.RESENT -> R.drawable.ic_check_green
+            else -> R.style.request_item_resent
+        }
+    }
+
+    private fun Contact.actionLabel(): String {
+        return when (RequestStatus.from(status)) {
+            RequestStatus.RESENT -> appContext().getString(R.string.request_item_action_resent)
+            else -> appContext().getString(R.string.request_item_action_retry)
+        }
+    }
+
+
+    private suspend fun resolveBitmap(data: ByteArray?): Bitmap? = withContext(Dispatchers.IO) {
+        BitmapResolver.getBitmap(data)
+    }
+
+    private suspend fun searchUd(factQuery: FactQuery) = flow {
+        val result = try {
+            val udResult = fetchUser(factQuery)
+            udResult.second?.let { // Error message
+                if (it.isNotEmpty()) {
+                    if (!it.contains("no results found", true)) {
+                        showError(it)
+                    }
+                    noResultPlaceholder(factQuery)
+                } else { // Search result
+                    udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery)
+                }
+            } ?: run {
+                udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery)
+            }
+        } catch (e: Exception) {
+            e.message?.let {
+                showError(genericSearchError)
+                couldNotCompletePlaceholder(it)
+            } ?: run {
+                couldNotCompletePlaceholder(genericSearchError)
+            }
+        }
+
+        changeStateTo(searchCompleteState)
+
+        emit(result)
+    }.stateIn(viewModelScope)
+
+    private suspend fun fetchUser(factQuery: FactQuery): Pair<ContactWrapperBase?, String?> {
+        return try {
+            repo.searchUd(factQuery.fact, factQuery.type).value()
+        } catch (e: NodeErrorException) {
+            delay(5000)
+            fetchUser(factQuery)
+        }
+    }
+
+    private fun ContactWrapperBase.asSearchResult(): RequestItem {
+        // ContactWrapperBase -> ContactRequestData
+        val requestData = ContactRequestData(
+            ContactData.from(this, RequestStatus.SEARCH)
+        )
+        // ContactRequestData -> RequestItem
+        return SearchResultItem(requestData)
+    }
+
+    private fun changeStateTo(ui: UdSearchUi) {
+        _udSearchUi.postValue(ui)
+    }
+
+    private fun showError(error: String) {
+        _toastUi.postValue(
+            ToastUI.create(
+                body = error,
+                leftIcon = R.drawable.ic_alert
+            )
+        )
+    }
+
+    fun onToastShown() {
+        _toastUi.value = null
+    }
+
+    fun onDialogShown() {
+        _dialogUi.value = null
+    }
+
+    private fun onPlaceholderClicked() {
+        _searchInfoDialog.value = searchInfoDialogUi
+    }
+
+    fun onInfoDialogShown() {
+        _searchInfoDialog.value = null
+    }
+
+    private fun onCancelSearchClicked() {
+        searchJob?.cancel()
+        changeStateTo(searchCompleteState)
+    }
+
+    private fun onCountryCodeClicked() {
+        _selectCountry.value = countryListener
+    }
+
+    val dismissCountries: LiveData<Boolean> by ::_dismissCountries
+    private val _dismissCountries = MutableLiveData(false)
+
+    private fun onCountrySelected(selectedCountry: Country?) {
+        _dismissCountries.value = true
+        country = selectedCountry ?: return
+    }
+
+    fun onCountriesDismissed() {
+        _dismissCountries.value = false
+    }
+
+    fun onUserInput(input: String?) {
+        _udSearchUi.value =
+            input?.let {
+                userInputState
+            } ?: initialState
+    }
+}
+
+private sealed class FactQuery {
+    abstract val fact: String
+    abstract val type: FactType
+
+    class UsernameQuery(query: String?): FactQuery() {
+        override val fact: String = query ?: ""
+        override val type: FactType = FactType.USERNAME
+    }
+
+    class EmailQuery(query: String?): FactQuery() {
+        override val fact: String = query ?: ""
+        override val type: FactType = FactType.EMAIL
+    }
+
+    class PhoneQuery(query: String?): FactQuery() {
+        override val fact: String = query ?: ""
+        override val type: FactType = FactType.PHONE
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt
index 99f5cd93980aee5bc0d49c8da204a2be72a71618..b680da37a37f6a26b503e78c18b8c8fdbf78949f 100644
--- a/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/dialog/info/InfoDialogLauncher.kt
@@ -1,6 +1,8 @@
 package io.xxlabs.messenger.ui.dialog.info
 
 import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
+import io.xxlabs.messenger.support.appContext
 
 /**
  * Launches an InfoDialog with a neutral button.
@@ -31,6 +33,30 @@ fun Fragment.showInfoDialog(
         .show(requireActivity().supportFragmentManager, null)
 }
 
+fun ViewModel.createInfoDialog(
+    title: Int,
+    body: Int,
+    linkTextToUrlMap: Map<String, String>? = null
+) : InfoDialogUI {
+    var spans: MutableList<SpanConfig>? = null
+    linkTextToUrlMap?.apply {
+        spans = mutableListOf()
+        for (entry in keys) {
+            val spanConfig = SpanConfig.create(
+                entry,
+                this[entry],
+            )
+            spans?.add(spanConfig)
+        }
+    }
+
+    return InfoDialogUI.create(
+        title = appContext().getString(title),
+        body = appContext().getString(body),
+        spans = spans,
+    )
+}
+
 /**
  * Launches an InfoDialog with a positive and negative button.
  */
@@ -68,3 +94,36 @@ fun Fragment.showTwoButtonInfoDialog(
     TwoButtonInfoDialog.newInstance(twoButtonUI)
         .show(parentFragmentManager, null)
 }
+
+fun ViewModel.createTwoButtonDialogUi(
+    title: Int,
+    body: Int,
+    linkTextToUrlMap: Map<String, String>? = null,
+    positiveClick: ()-> Unit,
+    negativeClick: (()-> Unit)? = null,
+    onDismiss: ()-> Unit = { },
+) : TwoButtonInfoDialogUI {
+    var spans: MutableList<SpanConfig>? = null
+    linkTextToUrlMap?.apply {
+        spans = mutableListOf()
+        for (entry in keys) {
+            val spanConfig = SpanConfig.create(
+                entry,
+                this[entry],
+            )
+            spans?.add(spanConfig)
+        }
+    }
+    val infoDialogUI = InfoDialogUI.create(
+        title = appContext().getString(title),
+        body = appContext().getString(body),
+        spans = spans,
+        onDismiss
+    )
+
+    return TwoButtonInfoDialogUI.create(
+        infoDialogUI,
+        onPositiveClick = positiveClick,
+        onNegativeClick = negativeClick
+    )
+}
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt
index a1dee2d293d8b04735b91a4cb2f092f80818fa1d..68bc86051046701fa28bb73cc400d609aed2159d 100644
--- a/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/splash/SplashScreenPlaceholderActivity.kt
@@ -19,7 +19,9 @@ import io.xxlabs.messenger.support.isMockVersion
 import io.xxlabs.messenger.support.util.Utils
 import io.xxlabs.messenger.ui.base.BaseInjectorActivity
 import io.xxlabs.messenger.ui.main.MainActivity
-import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_DEEP_LINK_BUNDLE
+import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_INVITATION
+import io.xxlabs.messenger.ui.main.MainActivity.Companion.INTENT_NOTIFICATION_CLICK
+import timber.log.Timber
 import javax.inject.Inject
 
 class SplashScreenPlaceholderActivity : BaseInjectorActivity() {
@@ -54,13 +56,33 @@ class SplashScreenPlaceholderActivity : BaseInjectorActivity() {
     }
 
     private fun handleIntent(intent: Intent) {
+        if (Intent.ACTION_VIEW == intent.action) {
+            // Implicit Intent from an invitation link
+            intent.data?.getQueryParameter("username")?.let { username ->
+                invitationIntent(username)
+            }
+        } else notificationIntent(intent)
+    }
+
+    private fun invitationIntent(username: String) {
+        // Invitations can only be handled if the user has an account.
+        if (preferencesRepository.name.isNotEmpty()) {
+            val intent = Intent(this, MainActivity::class.java).apply {
+                putExtra(INTENT_INVITATION, username)
+            }
+            startActivity(intent)
+            finish()
+        }
+    }
+
+    private fun notificationIntent(intent: Intent) {
         // Pass this intent on to MainActivity.
-        mainIntent = intent.getBundleExtra(INTENT_DEEP_LINK_BUNDLE)?.let {
+        mainIntent = intent.getBundleExtra(INTENT_NOTIFICATION_CLICK)?.let {
             Intent(
                 this@SplashScreenPlaceholderActivity,
                 MainActivity::class.java
             ).apply {
-                putExtra(INTENT_DEEP_LINK_BUNDLE, it)
+                putExtra(INTENT_NOTIFICATION_CLICK, it)
             }
         }
     }
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt
index 971108cb3d40ebac18b0478ac01c3a324592b9c9..64a04e9412732e12828f0405743dbc5e1966552c 100755
--- a/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/main/MainActivity.kt
@@ -4,7 +4,6 @@ import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.ObjectAnimator
 import android.annotation.SuppressLint
-import android.app.ProgressDialog.show
 import android.content.Context
 import android.content.Intent
 import android.graphics.BitmapFactory
@@ -20,12 +19,12 @@ import androidx.lifecycle.*
 import androidx.lifecycle.Observer
 import androidx.navigation.NavController
 import androidx.navigation.Navigation
+import androidx.navigation.findNavController
 import androidx.navigation.ui.onNavDestinationSelected
 import com.bumptech.glide.Glide
 import com.google.android.material.shape.CornerFamily
 import com.google.android.material.snackbar.BaseTransientBottomBar
 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_INDEFINITE
-import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
 import com.google.android.material.snackbar.Snackbar
 import io.xxlabs.messenger.BuildConfig
 import io.xxlabs.messenger.NavMainDirections
@@ -33,11 +32,9 @@ import io.xxlabs.messenger.R
 import io.xxlabs.messenger.bindings.wrapper.bindings.BindingsWrapperBindings
 import io.xxlabs.messenger.data.data.DataRequestState
 import io.xxlabs.messenger.data.data.SimpleRequestState
-import io.xxlabs.messenger.data.datatype.NetworkState
 import io.xxlabs.messenger.data.room.model.Contact
 import io.xxlabs.messenger.databinding.ComponentCustomToastBinding
 import io.xxlabs.messenger.media.MediaProviderActivity
-import io.xxlabs.messenger.notifications.MessagingService
 import io.xxlabs.messenger.support.callback.NetworkWatcher
 import io.xxlabs.messenger.support.dialog.PopupActionBottomDialog
 import io.xxlabs.messenger.support.extensions.*
@@ -61,7 +58,6 @@ import kotlinx.android.synthetic.main.component_menu.*
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import timber.log.Timber
-import java.util.*
 import javax.inject.Inject
 
 private val Bundle.isPrivateMessage: Boolean
@@ -168,12 +164,27 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv
     }
 
     private fun handleIntent(intent: Intent) {
-        intent.getBundleExtra(INTENT_DEEP_LINK_BUNDLE)?.let {
-            handleDeepLink(it)
+        intent.getBundleExtra(INTENT_NOTIFICATION_CLICK)?.let {
+            // PendingIntent from notifications
+            handleNotification(it)
+            return
         }
+
+        intent.getStringExtra(INTENT_INVITATION)?.let { username ->
+            // Implicit intent from an invitation link
+            invitationIntent(username)
+            return
+        }
+    }
+
+    private fun invitationIntent(username: String) {
+        val userSearch = NavMainDirections.actionGlobalConnectionInvitation().apply {
+            this.username = username
+        }
+        mainNavController.navigateSafe(userSearch)
     }
 
-    private fun handleDeepLink(bundle: Bundle) {
+    private fun handleNotification(bundle: Bundle) {
         with (bundle) {
             when {
                 isPrivateMessage -> privateMessageIntent(this)
@@ -361,6 +372,37 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv
             hideMenu()
             mainNavController.navigateSafe(R.id.action_global_ud_profile)
         }
+
+        menuShareText?.setOnSingleClickListener {
+            hideMenu()
+            sendInvitation()
+        }
+    }
+
+    private fun sendInvitation() {
+        val sendIntent: Intent = Intent().apply {
+            action = Intent.ACTION_SEND
+            putExtra(
+                Intent.EXTRA_TEXT,
+                getString(R.string.share_invitation_message, preferences.name)
+            )
+            type = "text/plain"
+        }
+
+        val title: String = getString(R.string.share_chooser_title)
+        val chooser: Intent = Intent.createChooser(sendIntent, title)
+
+        if (sendIntent.resolveActivity(packageManager) != null) {
+            startActivity(chooser)
+        } else {
+            showCustomToast(
+                ToastUI.create(
+                    body = getString(R.string.share_no_activity_error),
+                    leftIcon = R.drawable.ic_alert,
+                    iconTint = R.color.accent_danger
+                )
+            )
+        }
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -781,10 +823,11 @@ class MainActivity : MediaProviderActivity(), SnackBarActivity, CustomToastActiv
     }
 
     companion object : BaseInstance {
-        const val INTENT_DEEP_LINK_BUNDLE = "nav_bundle"
+        const val INTENT_NOTIFICATION_CLICK = "nav_bundle"
         const val INTENT_PRIVATE_CHAT = "private_message"
         const val INTENT_GROUP_CHAT = "group_message"
         const val INTENT_REQUEST = "request"
+        const val INTENT_INVITATION = "invitation"
 
         private var activeInstances = 0
         override fun activeInstancesCount(): Int {
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt
index 5cb03d40c26e2c484db0e1e22856484fe5ace446..2c2508af2320dbce72242db2d2a021bc933073fd 100644
--- a/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/main/ud/search/UdSearchFragment.kt
@@ -27,7 +27,6 @@ import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase
 import io.xxlabs.messenger.data.data.Country
 import io.xxlabs.messenger.data.data.DataRequestState
 import io.xxlabs.messenger.data.data.SimpleRequestState
-import io.xxlabs.messenger.data.datatype.NetworkState
 import io.xxlabs.messenger.data.room.model.ContactData
 import io.xxlabs.messenger.requests.ui.RequestsViewModel
 import io.xxlabs.messenger.requests.ui.nickname.SaveNicknameDialog
@@ -44,7 +43,6 @@ import io.xxlabs.messenger.ui.main.countrycode.CountryFullscreenDialog
 import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener
 import kotlinx.android.synthetic.main.component_toolbar_generic.*
 import kotlinx.android.synthetic.main.fragment_private_search.*
-import io.xxlabs.messenger.ui.dialog.info.showTwoButtonInfoDialog
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -418,14 +416,6 @@ class UdSearchFragment : BaseFragment() {
     }
 
     private fun watchForChanges() {
-//        networkViewModel.networkState.observe(viewLifecycleOwner, { networkState ->
-//            if (networkState != NetworkState.HAS_CONNECTION) {
-//                snackBar.show()
-//            } else {
-//                snackBar.dismiss()
-//            }
-//        })
-
         networkViewModel.userDiscoveryStatus.observe(
             viewLifecycleOwner,
             { isUdCompletelyInitialized ->
diff --git a/app/src/main/res/drawable/ic_connections.xml b/app/src/main/res/drawable/ic_connections.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4079f3f8406a42717da41af33e0947cad5bb0416
--- /dev/null
+++ b/app/src/main/res/drawable/ic_connections.xml
@@ -0,0 +1,31 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M6,3L18,3A1,1 0,0 1,19 4L19,20A1,1 0,0 1,18 21L6,21A1,1 0,0 1,5 20L5,4A1,1 0,0 1,6 3z"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#B1B5BA"/>
+  <path
+      android:pathData="M15,17C15,15.343 13.657,14 12,14C10.343,14 9,15.343 9,17"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#B1B5BA"/>
+  <path
+      android:pathData="M12,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#B1B5BA"/>
+  <path
+      android:pathData="M3,9L7,9"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#B1B5BA"/>
+  <path
+      android:pathData="M3,15L7,15"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#B1B5BA"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_dashboard_24px.xml b/app/src/main/res/drawable/ic_dashboard_24px.xml
new file mode 100644
index 0000000000000000000000000000000000000000..87968f4aa1dea8e80982856211d6a23aa52b37f8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dashboard_24px.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="18dp"
+    android:height="18dp"
+    android:viewportWidth="18"
+    android:viewportHeight="18">
+  <path
+      android:pathData="M0,0H8V10H0V0ZM18,0H10V6H18V0ZM6,8V2H2V8H6ZM16,4V2H12V4H16ZM16,10V16H12V10H16ZM6,16V14H2V16H6ZM18,8H10V18H18V8ZM0,12H8V18H0V12Z"
+      android:fillColor="#B1B5BA"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_qr_squared.xml b/app/src/main/res/drawable/ic_qr_squared.xml
index 75c7ec109b1aac8b4287d00c5bd51d9ca373966d..ab0db05f5756950739c6d64c106738efc4cec863 100644
--- a/app/src/main/res/drawable/ic_qr_squared.xml
+++ b/app/src/main/res/drawable/ic_qr_squared.xml
@@ -5,9 +5,9 @@
     android:viewportHeight="40">
   <path
       android:pathData="M8,0L32,0A8,8 0,0 1,40 8L40,32A8,8 0,0 1,32 40L8,40A8,8 0,0 1,0 32L0,8A8,8 0,0 1,8 0z"
-      android:fillColor="@color/neutral_body"/>
+      android:fillColor="@color/neutral_active"/>
   <path
       android:pathData="M11.875,14.5C11.875,13.1227 12.9977,12 14.375,12H16.625V14H14.375C14.1023,14 13.875,14.2273 13.875,14.5V16.75H11.875V14.5ZM25.625,14H23.375V12H25.625C27.0023,12 28.125,13.1227 28.125,14.5V16.75H26.125V14.5C26.125,14.2273 25.8977,14 25.625,14ZM13.875,25.75V23.5H11.875V25.75C11.875,27.1273 12.9977,28.25 14.375,28.25H16.625V26.25H14.375C14.1023,26.25 13.875,26.0227 13.875,25.75ZM28.125,23.5V25.75C28.125,27.1273 27.0023,28.25 25.625,28.25H23.375V26.25H25.625C25.8977,26.25 26.125,26.0227 26.125,25.75V23.5H28.125ZM11,21.125H29V19.125H11V21.125Z"
-      android:fillColor="#242424"
+      android:fillColor="@color/neutral_white"
       android:fillType="evenOdd"/>
 </vector>
diff --git a/app/src/main/res/drawable/ic_requests.xml b/app/src/main/res/drawable/ic_requests.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4d9fbd5eb27bb5a6c590ff2c16a8db26f0228113
--- /dev/null
+++ b/app/src/main/res/drawable/ic_requests.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M19.2,19.4H4.8C3.806,19.4 3,18.594 3,17.6V6.722C3.042,5.758 3.836,4.999 4.8,5H19.2C20.194,5 21,5.806 21,6.8V17.6C21,18.594 20.194,19.4 19.2,19.4ZM4.8,8.481V17.6H19.2V8.481L12,13.28L4.8,8.481ZM5.52,6.8L12,11.12L18.48,6.8H5.52Z"
+      android:fillColor="#B1B5BA"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 0000000000000000000000000000000000000000..098c40b4d863e371b67af6a7edc774b729301eb6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M17.6,18.199V15.498C17.6,15.001 18.003,14.598 18.5,14.598C18.997,14.598 19.4,15.001 19.4,15.498V18.199C19.4,19.192 18.594,20 17.604,20H6.796C5.803,20 5,19.196 5,18.199V15.498C5,15.001 5.403,14.598 5.9,14.598C6.397,14.598 6.8,15.001 6.8,15.498L6.796,18.199L17.6,18.199Z"
+      android:fillColor="#B1B5BA"/>
+  <path
+      android:pathData="M14.109,8.079L13.103,7.072L13.103,15.49C13.103,15.987 12.703,16.391 12.203,16.391C11.706,16.391 11.303,15.988 11.303,15.49L11.303,7.072L10.296,8.079C9.943,8.432 9.375,8.436 9.021,8.081C8.669,7.73 8.671,7.158 9.023,6.805L11.564,4.264C11.742,4.086 11.972,3.999 12.202,3.999C12.433,3.997 12.663,4.085 12.839,4.262C12.841,4.263 15.382,6.805 15.382,6.805C15.735,7.159 15.739,7.727 15.385,8.081C15.033,8.433 14.461,8.431 14.109,8.079Z"
+      android:fillColor="#B1B5BA"/>
+</vector>
diff --git a/app/src/main/res/drawable/white_rounded_square_outline.xml b/app/src/main/res/drawable/white_rounded_square_outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5e52b28c38a41e2e39df01c33ca7e1d0d4c84557
--- /dev/null
+++ b/app/src/main/res/drawable/white_rounded_square_outline.xml
@@ -0,0 +1,5 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/spacing_30"/>
+    <stroke android:width="2dp" android:color="@color/white"/>
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/layout/component_menu.xml b/app/src/main/res/layout/component_menu.xml
index 6f964e4af1d690820d6f09f7bbc1fb3bc02b62e6..96d721f939ee86793e1652dd54f06143ae2928cf 100644
--- a/app/src/main/res/layout/component_menu.xml
+++ b/app/src/main/res/layout/component_menu.xml
@@ -129,8 +129,7 @@
             app:layout_constraintEnd_toEndOf="@id/menuGuideEnd"
             app:layout_constraintHorizontal_bias="1"
             app:layout_constraintStart_toEndOf="@id/menuBarrierUsername"
-            app:layout_constraintTop_toTopOf="@id/menuTopText"
-            app:layout_constraintVertical_bias="0"/>
+            app:layout_constraintTop_toTopOf="@id/menuTopText" />
 
         <!-- CHATS -->
         <ImageView
@@ -163,7 +162,7 @@
         <!-- CONTACTS -->
         <ImageView
             android:id="@+id/menuContactsIcon"
-            android:src="@drawable/ic_menu_contacts"
+            android:src="@drawable/ic_connections"
             app:layout_constraintStart_toEndOf="@id/menuGuideStart"
             app:layout_constraintTop_toBottomOf="@id/menuChatsText"
             style="@style/menu_icon" />
@@ -188,7 +187,7 @@
         <!-- REQUESTS -->
         <ImageView
             android:id="@+id/menuContactRequestsIcon"
-            android:src="@drawable/ic_mail"
+            android:src="@drawable/ic_requests"
             app:layout_constraintStart_toEndOf="@id/menuGuideStart"
             app:layout_constraintTop_toBottomOf="@id/menuContactsTxt"
             style="@style/menu_icon" />
@@ -262,11 +261,12 @@
         <!-- SCAN -->
 
         <!-- SETTINGS -->
+
         <ImageView
             android:id="@+id/menuSettingsIcon"
             android:src="@drawable/ic_menu_settings"
             app:layout_constraintStart_toEndOf="@id/menuGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/menuJoinXxTxt"
+            app:layout_constraintTop_toBottomOf="@id/menuScanText"
             style="@style/menu_icon" />
 
         <TextView
@@ -290,9 +290,9 @@
             android:layout_height="@dimen/spacing_24"
             android:layout_marginTop="@dimen/spacing_24"
             android:padding="@dimen/spacing_3"
-            android:src="@drawable/ic_dashboard"
+            android:src="@drawable/ic_dashboard_24px"
             app:layout_constraintStart_toEndOf="@id/menuGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/menuScanText"
+            app:layout_constraintTop_toBottomOf="@id/menuSettingsTxt"
             style="@style/menu_icon" />
 
         <TextView
@@ -336,6 +336,31 @@
             app:layout_constraintStart_toEndOf="@id/menuJoinXxIcon"
             app:layout_constraintTop_toTopOf="@id/menuJoinXxIcon" />
 
+        <ImageView
+            android:id="@+id/menuShareIcon"
+            android:layout_width="@dimen/spacing_24"
+            android:layout_height="@dimen/spacing_24"
+            android:layout_marginTop="@dimen/spacing_24"
+            android:src="@drawable/ic_share"
+            app:layout_constraintStart_toEndOf="@id/menuGuideStart"
+            app:layout_constraintTop_toBottomOf="@id/menuJoinXxTxt"
+            style="@style/menu_icon" />
+
+        <TextView
+            android:id="@+id/menuShareText"
+            style="@style/XxTextStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/spacing_10"
+            android:contentDescription="menu.option.dashboard"
+            android:padding="@dimen/spacing_8"
+            android:text="Share my profile"
+            android:textColor="@color/neutral_weak"
+            android:textSize="@dimen/text_14"
+            app:layout_constraintBottom_toBottomOf="@id/menuShareIcon"
+            app:layout_constraintStart_toEndOf="@id/menuShareIcon"
+            app:layout_constraintTop_toTopOf="@id/menuShareIcon" />
+
         <TextView
             android:id="@+id/menuBottomBuildTxt"
             style="@style/XxTextStyle.SemiBold"
@@ -348,7 +373,7 @@
             android:textSize="@dimen/text_12"
             app:layout_constraintBottom_toTopOf="@id/menuBottomVersionTxt"
             app:layout_constraintStart_toEndOf="@id/menuGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/menuSettingsTxt"
+            app:layout_constraintTop_toBottomOf="@id/menuShareText"
             app:layout_constraintVertical_bias="1"
             app:layout_constraintVertical_chainStyle="packed"
             tools:text="Build 1.1.0" />
diff --git a/app/src/main/res/layout/component_toolbar_generic.xml b/app/src/main/res/layout/component_toolbar_generic.xml
index 91641055cabd1160616459cb6ca69aa609df520f..e855c5b92bbae0e50ed304da4aa2a285e97a4d13 100644
--- a/app/src/main/res/layout/component_toolbar_generic.xml
+++ b/app/src/main/res/layout/component_toolbar_generic.xml
@@ -120,6 +120,7 @@
             android:layout_width="0dp"
             android:layout_height="1dp"
             android:background="@color/neutral_line"
+            android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent" />
diff --git a/app/src/main/res/layout/fragment_chats_list.xml b/app/src/main/res/layout/fragment_chats_list.xml
index a9089f9e9c6ebcdf648e3b3b2262c3af9533a3e5..773edfdebd1a6778471f403db8527091bb8d34b2 100755
--- a/app/src/main/res/layout/fragment_chats_list.xml
+++ b/app/src/main/res/layout/fragment_chats_list.xml
@@ -140,6 +140,7 @@
             android:layout_height="1dp"
             android:layout_marginTop="@dimen/spacing_12"
             android:background="@color/neutral_line"
+            android:visibility="gone"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/chatsMenu" />
diff --git a/app/src/main/res/layout/fragment_fact_search.xml b/app/src/main/res/layout/fragment_fact_search.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4784c1aada3699e8383b408e9dc8e2c7ff744a7c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_fact_search.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+        <variable
+            name="ui"
+            type="io.xxlabs.messenger.search.FactSearchUi" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/countryButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="ud.search.input.dial.code"
+            android:textColor="@color/textInputFieldActive"
+            android:textSize="@dimen/text_14"
+            android:visibility="@{ui.countryCode}"
+            android:text="@{ui.countryCode}"
+            android:background="@drawable/bg_rectangle_rounded_corners_big_radius"
+            android:backgroundTint="@color/neutral_off_white"
+            android:padding="10dp"
+            android:paddingHorizontal="12dp"
+            android:onClick="@{() -> ui.onCountryClicked()}"
+            app:layout_constraintBottom_toBottomOf="@id/searchTextInput"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/searchTextInput"
+            tools:visibility="visible"
+            tools:text="🇺🇸 +1">
+        </com.google.android.material.textview.MaterialTextView>
+
+        <com.google.android.material.textfield.TextInputLayout
+            android:id="@+id/searchTextInput"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/spacing_16"
+            android:layout_marginStart="8dp"
+            app:layout_goneMarginStart="0dp"
+            app:endIconMode="clear_text"
+            app:errorEnabled="false"
+            app:helperTextEnabled="false"
+            app:hintEnabled="false"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toEndOf="@id/countryButton"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/searchTextInputEditText"
+                style="@style/InputEditText.Search"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/spacing_40"
+                android:layout_gravity="center"
+                android:contentDescription="chats.input.search"
+                android:hint="@{ui.searchHint}"
+                android:afterTextChanged="@{ui::onSearchInput}"
+                android:enabled="@{ui.userInputEnabled}"
+                android:maxHeight="@dimen/spacing_40"
+                app:layout_goneMarginTop="@dimen/spacing_28" />
+        </com.google.android.material.textfield.TextInputLayout>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/searchResultsRV"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/registration_body_vertical_margin"
+            app:layout_constraintTop_toBottomOf="@id/searchTextInput"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_private_search.xml b/app/src/main/res/layout/fragment_private_search.xml
index 1f7a744816d1c69fec647cd316c0a6b354970bef..56a59c60fc69a202441007943aad8670e8dbe251 100644
--- a/app/src/main/res/layout/fragment_private_search.xml
+++ b/app/src/main/res/layout/fragment_private_search.xml
@@ -1,206 +1,214 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/background"
-    android:fillViewport="true">
+    xmlns:tools="http://schemas.android.com/tools">
 
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/udSearch"
+    <data>
+        <variable
+            name="ui"
+            type="io.xxlabs.messenger.search.UdSearchUi" />
+    </data>
+
+    <androidx.core.widget.NestedScrollView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:descendantFocusability="beforeDescendants"
-        android:focusableInTouchMode="true">
-
-        <include layout="@layout/component_toolbar_generic" />
+        android:background="@color/background"
+        android:fillViewport="true">
 
-        <LinearLayout
-            android:id="@+id/udSearchUsernameLayout"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/spacing_20"
-            android:contentDescription="ud.search.selection.username"
-            android:gravity="center_horizontal"
-            android:orientation="horizontal"
-            android:padding="@dimen/spacing_12"
-            app:layout_constraintEnd_toStartOf="@id/udSearchEmailLayout"
-            app:layout_constraintHorizontal_bias="0"
-            app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/toolbarGeneric">
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/udSearch"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:descendantFocusability="beforeDescendants"
+            android:focusableInTouchMode="true">
 
-            <ImageView
-                android:id="@+id/udSearchUsernameIcon"
-                android:layout_width="@dimen/spacing_20"
-                android:layout_height="@dimen/spacing_20"
-                android:src="@drawable/ic_contact_light" />
+            <include layout="@layout/component_toolbar_generic" />
 
-            <TextView
-                android:id="@+id/udSearchUsernameText"
-                style="@style/XxTextStyle.SemiBold"
+            <LinearLayout
+                android:id="@+id/udSearchUsernameLayout"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginStart="@dimen/spacing_8"
-                android:text="Username"
-                android:textColor="@color/neutral_dark"
-                android:textSize="@dimen/text_14" />
-        </LinearLayout>
+                android:layout_marginTop="@dimen/spacing_20"
+                android:contentDescription="ud.search.selection.username"
+                android:gravity="center_horizontal"
+                android:orientation="horizontal"
+                android:padding="@dimen/spacing_12"
+                app:layout_constraintEnd_toStartOf="@id/udSearchEmailLayout"
+                app:layout_constraintHorizontal_bias="0"
+                app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
+                app:layout_constraintTop_toBottomOf="@id/toolbarGeneric">
 
-        <LinearLayout
-            android:id="@+id/udSearchEmailLayout"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="@dimen/spacing_16"
-            android:gravity="center_horizontal"
-            android:contentDescription="ud.search.selection.email"
-            android:orientation="horizontal"
-            android:padding="@dimen/spacing_12"
-            app:layout_constraintEnd_toStartOf="@id/udSearchPhoneLayout"
-            app:layout_constraintStart_toEndOf="@id/udSearchUsernameLayout"
-            app:layout_constraintTop_toTopOf="@id/udSearchUsernameLayout">
+                <ImageView
+                    android:id="@+id/udSearchUsernameIcon"
+                    android:layout_width="@dimen/spacing_20"
+                    android:layout_height="@dimen/spacing_20"
+                    android:src="@drawable/ic_contact_light" />
+
+                <TextView
+                    android:id="@+id/udSearchUsernameText"
+                    style="@style/XxTextStyle.SemiBold"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_8"
+                    android:text="Username"
+                    android:textColor="@color/neutral_dark"
+                    android:textSize="@dimen/text_14" />
+            </LinearLayout>
 
-            <ImageView
-                android:id="@+id/udSearchEmailIcon"
+            <LinearLayout
+                android:id="@+id/udSearchEmailLayout"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:src="@drawable/ic_mail"
-                app:tint="@color/neutral_dark" />
+                android:layout_marginStart="@dimen/spacing_16"
+                android:gravity="center_horizontal"
+                android:contentDescription="ud.search.selection.email"
+                android:orientation="horizontal"
+                android:padding="@dimen/spacing_12"
+                app:layout_constraintEnd_toStartOf="@id/udSearchPhoneLayout"
+                app:layout_constraintStart_toEndOf="@id/udSearchUsernameLayout"
+                app:layout_constraintTop_toTopOf="@id/udSearchUsernameLayout">
 
-            <TextView
-                android:id="@+id/udSearchEmailText"
-                style="@style/XxTextStyle.SemiBold"
+                <ImageView
+                    android:id="@+id/udSearchEmailIcon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@drawable/ic_mail"
+                    app:tint="@color/neutral_dark" />
+
+                <TextView
+                    android:id="@+id/udSearchEmailText"
+                    style="@style/XxTextStyle.SemiBold"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_8"
+                    android:text="Email"
+                    android:textColor="@color/neutral_dark"
+                    android:textSize="@dimen/text_14" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/udSearchPhoneLayout"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/spacing_16"
+                android:gravity="center_horizontal"
+                android:orientation="horizontal"
+                android:contentDescription="ud.search.selection.phone"
+                android:padding="@dimen/spacing_12"
+                app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
+                app:layout_constraintStart_toEndOf="@id/udSearchEmailLayout"
+                app:layout_constraintTop_toTopOf="@id/udSearchEmailLayout">
+
+                <ImageView
+                    android:id="@+id/udSearchPhoneIcon"
+                    android:layout_width="@dimen/spacing_20"
+                    android:layout_height="@dimen/spacing_20"
+                    android:src="@drawable/ic_phone"
+                    app:tint="@color/neutral_dark" />
+
+                <TextView
+                    android:id="@+id/udSearchPhoneText"
+                    style="@style/XxTextStyle.SemiBold"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_8"
+                    android:text="Phone"
+                    android:textColor="@color/neutral_dark"
+                    android:textSize="@dimen/text_14" />
+            </LinearLayout>
+
+            <androidx.appcompat.widget.AppCompatEditText
+                android:id="@+id/udSearchInput"
+                style="@style/InputEditText.Search.Squared"
+                android:layout_width="0dp"
+                android:layout_height="@dimen/spacing_40"
+                android:contentDescription="ud.search.input.default"
                 android:layout_marginStart="@dimen/spacing_8"
-                android:text="Email"
-                android:textColor="@color/neutral_dark"
-                android:textSize="@dimen/text_14" />
-        </LinearLayout>
+                android:layout_marginTop="@dimen/spacing_16"
+                android:hint="@string/search_hint"
+                android:imeOptions="actionSearch"
+                android:inputType="textNoSuggestions"
+                android:lines="1"
+                android:maxHeight="@dimen/spacing_40"
+                android:textSize="@dimen/text_14"
+                app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
+                app:layout_constraintStart_toEndOf="@id/udSearchInputPhoneCode"
+                app:layout_constraintTop_toBottomOf="@id/udSearchBarrier" />
 
-        <LinearLayout
-            android:id="@+id/udSearchPhoneLayout"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="@dimen/spacing_16"
-            android:gravity="center_horizontal"
-            android:orientation="horizontal"
-            android:contentDescription="ud.search.selection.phone"
-            android:padding="@dimen/spacing_12"
-            app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
-            app:layout_constraintStart_toEndOf="@id/udSearchEmailLayout"
-            app:layout_constraintTop_toTopOf="@id/udSearchEmailLayout">
+            <androidx.appcompat.widget.AppCompatEditText
+                android:id="@+id/udSearchInputPhoneCode"
+                style="@style/InputEditText"
+                android:layout_width="wrap_content"
+                android:layout_height="0dp"
+                android:contentDescription="ud.search.input.dial.code"
+                android:cursorVisible="false"
+                android:focusable="false"
+                android:focusableInTouchMode="false"
+                android:hint="🇺🇸 +1"
+                android:maxLength="10"
+                android:minEms="4"
+                android:textColor="@color/textInputFieldActive"
+                android:textSize="@dimen/text_14"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="@id/udSearchInput"
+                app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
+                app:layout_constraintTop_toTopOf="@id/udSearchInput" />
 
-            <ImageView
-                android:id="@+id/udSearchPhoneIcon"
+            <androidx.core.widget.ContentLoadingProgressBar
+                android:id="@+id/udSearchLoading"
+                style="@style/XxProgressBarCircularBlue"
                 android:layout_width="@dimen/spacing_20"
                 android:layout_height="@dimen/spacing_20"
-                android:src="@drawable/ic_phone"
-                app:tint="@color/neutral_dark" />
+                android:layout_marginEnd="@dimen/spacing_16"
+                app:layout_constraintBottom_toBottomOf="@id/udSearchInput"
+                app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
+                app:layout_constraintTop_toTopOf="@id/udSearchInput" />
 
             <TextView
-                android:id="@+id/udSearchPhoneText"
-                style="@style/XxTextStyle.SemiBold"
-                android:layout_width="wrap_content"
+                android:id="@+id/udSearchResultsTitle"
+                style="@style/XxTextStyle.Bold"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:layout_marginStart="@dimen/spacing_8"
-                android:text="Phone"
-                android:textColor="@color/neutral_dark"
-                android:textSize="@dimen/text_14" />
-        </LinearLayout>
-
-        <androidx.appcompat.widget.AppCompatEditText
-            android:id="@+id/udSearchInput"
-            style="@style/InputEditText.Search.Squared"
-            android:layout_width="0dp"
-            android:layout_height="@dimen/spacing_40"
-            android:contentDescription="ud.search.input.default"
-            android:layout_marginStart="@dimen/spacing_8"
-            android:layout_marginTop="@dimen/spacing_16"
-            android:hint="@string/search_hint"
-            android:imeOptions="actionSearch"
-            android:inputType="textNoSuggestions"
-            android:lines="1"
-            android:maxHeight="@dimen/spacing_40"
-            android:textSize="@dimen/text_14"
-            app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
-            app:layout_constraintStart_toEndOf="@id/udSearchInputPhoneCode"
-            app:layout_constraintTop_toBottomOf="@id/udSearchBarrier"/>
-
-        <androidx.appcompat.widget.AppCompatEditText
-            android:id="@+id/udSearchInputPhoneCode"
-            style="@style/InputEditText"
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
-            android:contentDescription="ud.search.input.dial.code"
-            android:cursorVisible="false"
-            android:focusable="false"
-            android:focusableInTouchMode="false"
-            android:hint="🇺🇸 +1"
-            android:maxLength="10"
-            android:minEms="4"
-            android:textColor="@color/textInputFieldActive"
-            android:textSize="@dimen/text_14"
-            android:visibility="gone"
-            app:layout_constraintBottom_toBottomOf="@id/udSearchInput"
-            app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
-            app:layout_constraintTop_toTopOf="@id/udSearchInput" />
-
-        <androidx.core.widget.ContentLoadingProgressBar
-            android:id="@+id/udSearchLoading"
-            style="@style/XxProgressBarCircularBlue"
-            android:layout_width="@dimen/spacing_20"
-            android:layout_height="@dimen/spacing_20"
-            android:layout_marginEnd="@dimen/spacing_16"
-            app:layout_constraintBottom_toBottomOf="@id/udSearchInput"
-            app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
-            app:layout_constraintTop_toTopOf="@id/udSearchInput" />
-
-        <TextView
-            android:id="@+id/udSearchResultsTitle"
-            style="@style/XxTextStyle.Bold"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/spacing_24"
-            android:paddingStart="@dimen/spacing_24"
-            android:paddingTop="@dimen/spacing_5"
-            android:paddingEnd="@dimen/spacing_24"
-            android:paddingBottom="@dimen/spacing_5"
-            android:text="Users"
-            android:textColor="@color/neutral_active"
-            android:textSize="@dimen/text_24"
-            android:visibility="gone"
-            app:layout_constraintBottom_toTopOf="@id/udSearchResultsRecyclerView"
-            app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
-            app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/udSearchInput"
-            app:layout_constraintVertical_bias="0" />
+                android:layout_marginTop="@dimen/spacing_24"
+                android:paddingStart="@dimen/spacing_24"
+                android:paddingTop="@dimen/spacing_5"
+                android:paddingEnd="@dimen/spacing_24"
+                android:paddingBottom="@dimen/spacing_5"
+                android:text="Users"
+                android:textColor="@color/neutral_active"
+                android:textSize="@dimen/text_24"
+                android:visibility="gone"
+                app:layout_constraintBottom_toTopOf="@id/udSearchResultsRecyclerView"
+                app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
+                app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
+                app:layout_constraintTop_toBottomOf="@id/udSearchInput"
+                app:layout_constraintVertical_bias="0" />
 
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/udSearchResultsRecyclerView"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginTop="@dimen/spacing_34"
-            android:layout_marginBottom="@dimen/spacing_16"
-            android:background="@color/backgroundSecondary"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="0.0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/udSearchResultsTitle"
-            app:layout_constraintVertical_bias="0"
-            tools:itemCount="2"
-            tools:listitem="@layout/list_item_contact_search_result" />
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/udSearchResultsRecyclerView"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_marginTop="@dimen/spacing_34"
+                android:layout_marginBottom="@dimen/spacing_16"
+                android:background="@color/backgroundSecondary"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="0.0"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/udSearchResultsTitle"
+                app:layout_constraintVertical_bias="0"
+                tools:itemCount="2"
+                tools:listitem="@layout/list_item_contact_search_result" />
 
-        <androidx.constraintlayout.widget.Barrier
-            android:id="@+id/udSearchBarrier"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:barrierDirection="bottom"
-            app:constraint_referenced_ids="udSearchUsernameLayout, udSearchEmailLayout, udSearchPhoneLayout"
-            tools:layout_editor_absoluteY="121dp" />
+            <androidx.constraintlayout.widget.Barrier
+                android:id="@+id/udSearchBarrier"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:barrierDirection="bottom"
+                app:constraint_referenced_ids="udSearchUsernameLayout, udSearchEmailLayout, udSearchPhoneLayout"
+                tools:layout_editor_absoluteY="121dp" />
 
 
         <TextView
@@ -212,7 +220,6 @@
             android:layout_marginTop="156dp"
             android:clickable="true"
             android:contentDescription="ud.search.empty.text"
-            android:drawableBottom="@drawable/ic_outline_info_24"
             android:focusable="true"
             android:gravity="center_horizontal"
             android:letterSpacing="0.01"
@@ -224,43 +231,44 @@
             app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
             app:layout_constraintTop_toBottomOf="@id/udSearchInput" />
 
-        <TextView
-            android:id="@+id/udSearchInputEmptyMessage"
-            style="@style/XxTextStyle.SemiBold"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/spacing_50"
-            android:gravity="center"
-            android:letterSpacing="0.01"
-            android:textColor="@color/neutral_body"
-            android:textSize="@dimen/text_14"
-            android:visibility="gone"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
-            app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
-            app:layout_constraintTop_toBottomOf="@id/udSearchInput"
-            app:layout_constraintVertical_bias="0.2"
-            tools:text="Abcdef" />
+            <TextView
+                android:id="@+id/udSearchInputEmptyMessage"
+                style="@style/XxTextStyle.SemiBold"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_50"
+                android:gravity="center"
+                android:letterSpacing="0.01"
+                android:textColor="@color/neutral_body"
+                android:textSize="@dimen/text_14"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="@id/udSearchGuideEnd"
+                app:layout_constraintStart_toStartOf="@id/udSearchGuideStart"
+                app:layout_constraintTop_toBottomOf="@id/udSearchInput"
+                app:layout_constraintVertical_bias="0.2"
+                tools:text="Abcdef" />
 
-        <androidx.constraintlayout.widget.Guideline
-            android:id="@+id/udSearchGuideStart"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            app:layout_constraintGuide_percent=".05" />
+            <androidx.constraintlayout.widget.Guideline
+                android:id="@+id/udSearchGuideStart"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                app:layout_constraintGuide_percent=".05" />
 
-        <androidx.constraintlayout.widget.Guideline
-            android:id="@+id/udSearchGuideMiddle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            app:layout_constraintGuide_percent="0.50" />
+            <androidx.constraintlayout.widget.Guideline
+                android:id="@+id/udSearchGuideMiddle"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                app:layout_constraintGuide_percent="0.50" />
 
-        <androidx.constraintlayout.widget.Guideline
-            android:id="@+id/udSearchGuideEnd"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            app:layout_constraintGuide_percent=".95" />
-    </androidx.constraintlayout.widget.ConstraintLayout>
-</androidx.core.widget.NestedScrollView>
\ No newline at end of file
+            <androidx.constraintlayout.widget.Guideline
+                android:id="@+id/udSearchGuideEnd"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                app:layout_constraintGuide_percent=".95" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </androidx.core.widget.NestedScrollView>
+</layout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_user_search.xml b/app/src/main/res/layout/fragment_user_search.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e66bfcd667569654c01fe51f507ed38361570979
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_search.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <data>
+        <variable
+            name="ui"
+            type="io.xxlabs.messenger.search.UdSearchUi" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:descendantFocusability="beforeDescendants"
+        android:focusableInTouchMode="true">
+
+        <include layout="@layout/component_toolbar_generic" />
+
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/userSearchAppBarLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/transparent"
+            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+            app:elevation="0dp"
+            app:layout_constraintTop_toBottomOf="@id/toolbarGeneric">
+
+            <com.google.android.material.tabs.TabLayout
+                android:id="@+id/userSearchAppBarTabs"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/transparent"
+                app:tabGravity="fill"
+                app:tabIconTint="@color/selector_qr_code_tab_secondary"
+                app:tabIndicatorFullWidth="true"
+                app:tabIndicatorHeight="@dimen/spacing_3"
+                app:tabInlineLabel="false"
+                app:tabMaxWidth="0dp"
+                app:tabMode="fixed"
+                app:tabPaddingEnd="8dp"
+                app:tabPaddingStart="8dp"
+                app:tabTextAppearance="@style/TabTextStyle"
+                app:tabTextColor="@color/selector_qr_code_tab_secondary" />
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.viewpager2.widget.ViewPager2
+            android:id="@+id/userSearchViewPager"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+            android:paddingHorizontal="@dimen/spacing_14"
+            android:paddingBottom="@dimen/spacing_14"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/userSearchAppBarLayout" />
+
+        <TextView
+            android:id="@+id/userSearchCallToAction"
+            style="@style/dialog_title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_34"
+            android:letterSpacing="0.01"
+            android:text="@{ui.callToActionText}"
+            tools:text="@string/search_call_to_action"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:lineHeight="40sp" />
+
+        <TextView
+            android:id="@+id/userSearchInfoText"
+            style="@style/XxTextStyle"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_34"
+            android:layout_marginTop="0dp"
+            android:clickable="true"
+            android:contentDescription="ud.search.empty.text"
+            android:focusable="true"
+            android:letterSpacing="0.01"
+            android:text="@{ui.placeholderText}"
+            android:textColor="@color/neutral_body"
+            android:textSize="@dimen/text_18"
+            android:textStyle="normal"
+            android:onClick="@{() -> ui.onPlaceholderClicked()}"
+            tools:text="@string/search_placeholder_text"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/userSearchCallToAction" />
+
+        <View
+            android:id="@+id/fadeView"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:alpha=".8"
+            android:background="@color/black"
+            android:visibility="@{ui.isSearching}"
+            android:elevation="5dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:visibility="visible" />
+
+        <ProgressBar
+            android:id="@+id/progressbar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            android:visibility="@{ui.isSearching}"
+            android:elevation="5dp"
+            android:indeterminateTint="@color/white"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <Button
+            android:id="@+id/userSearchCancelButton"
+            style="@style/dialog_button"
+            android:layout_width="0dp"
+            android:layout_marginBottom="@dimen/registration_body_vertical_margin"
+            android:elevation="5dp"
+            android:onClick="@{() -> ui.onCancelSearchClicked()}"
+            android:stateListAnimator="@null"
+            android:text="@string/cancel"
+            android:visibility="@{ui.isSearching}"
+            android:background="@drawable/white_rounded_square_outline"
+            android:textColor="@color/white"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/progressbar"
+            app:layout_constraintVertical_bias="0.75" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_request.xml b/app/src/main/res/layout/list_item_request.xml
index 66d700bf9fe2a7b4b84f12284df978b51ffc9b4f..d0dc187fc3269c5ff653f2e942e8e94e64743b10 100644
--- a/app/src/main/res/layout/list_item_request.xml
+++ b/app/src/main/res/layout/list_item_request.xml
@@ -33,7 +33,9 @@
             style="@style/request_item_header"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
             android:text="@{ui.title}"
+            app:layout_constraintBottom_toTopOf="@+id/requestSubtitle"
             app:layout_constraintEnd_toStartOf="@id/requestAction"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
@@ -47,6 +49,7 @@
             android:layout_height="wrap_content"
             android:text="@{ui.subtitle}"
             android:visibility="@{ui.subtitle}"
+            app:layout_constraintBottom_toTopOf="@+id/requestDetails"
             app:layout_constraintEnd_toStartOf="@id/requestAction"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
@@ -58,9 +61,11 @@
             style="@style/request_item_body"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
             android:lineSpacingExtra="2sp"
             android:text="@{ui.details}"
             android:visibility="@{ui.details}"
+            app:layout_constraintBottom_toTopOf="@+id/requestTimestamp"
             app:layout_constraintEnd_toStartOf="@id/requestAction"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
@@ -72,7 +77,9 @@
             style="@style/request_item_timestamp"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
             app:date="@{ui.timestamp}"
+            app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@id/requestAction"
             app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
diff --git a/app/src/main/res/layout/list_item_request_search_result.xml b/app/src/main/res/layout/list_item_request_search_result.xml
new file mode 100644
index 0000000000000000000000000000000000000000..84d3ecc71665c8514dd0181fb6e9690611c05578
--- /dev/null
+++ b/app/src/main/res/layout/list_item_request_search_result.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+        <import type="io.xxlabs.messenger.ui.main.chat.DrawablePosition"/>
+        <variable
+            name="ui"
+            type="io.xxlabs.messenger.requests.ui.list.adapter.ContactRequestSearchResultItem" />
+        <variable
+            name="listener"
+            type="io.xxlabs.messenger.requests.ui.list.adapter.RequestItemListener" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/requestItemLayout"
+        android:padding="16dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="@{() -> listener.onItemClicked(ui)}" >
+
+        <include
+            android:id="@+id/requestProfilePhoto"
+            layout="@layout/component_item_thumbnail"
+            android:layout_width="@dimen/spacing_42"
+            android:layout_height="@dimen/spacing_42"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:ui="@{ui}" />
+
+        <TextView
+            android:id="@+id/requestTitle"
+            style="@style/request_item_header"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:text="@{ui.title}"
+            app:layout_constraintBottom_toTopOf="@+id/requestSubtitle"
+            app:layout_constraintEnd_toStartOf="@id/requestAction"
+            app:layout_constraintHorizontal_bias="0.0"
+            app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
+            app:layout_constraintTop_toTopOf="@id/requestProfilePhoto"
+            tools:text="bartender007" />
+
+        <TextView
+            android:id="@+id/requestSubtitle"
+            style="@style/request_item_subheader"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@{ui.subtitle}"
+            android:visibility="@{ui.subtitle}"
+            android:textColor="@{ui.statusTextColor}"
+            app:layout_constraintEnd_toStartOf="@id/requestAction"
+            app:layout_constraintHorizontal_bias="0.0"
+            app:layout_constraintStart_toEndOf="@id/requestProfilePhoto"
+            app:layout_constraintTop_toBottomOf="@id/requestTitle"
+            tools:text="Request pending"
+            tools:textColor="@color/neutral_weak"/>
+
+        <io.xxlabs.messenger.support.view.SingleClickTextView
+            android:id="@+id/requestAction"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@{ui.actionLabel}"
+            android:padding="12dp"
+            android:layout_marginHorizontal="0dp"
+            android:drawablePadding="4dp"
+            android:onClick="@{() -> listener.onActionClicked(ui)}"
+            android:visibility="@{ui.actionVisible}"
+            android:singleLine="false"
+            android:gravity="center_vertical|end"
+            app:actionIcon="@{ui.actionIcon}"
+            app:iconColor="@{ui.actionIconColor}"
+            app:iconPosition="@{DrawablePosition.START}"
+            app:customStyle="@{ui.actionTextStyle}"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            tools:drawableLeft="@drawable/ic_retry"
+            tools:text="Resend"
+            style="@style/request_item_subheader"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_section_divider.xml b/app/src/main/res/layout/list_item_section_divider.xml
new file mode 100644
index 0000000000000000000000000000000000000000..30cb0cc5283f5cad52ab95c47a0e9c449a782e23
--- /dev/null
+++ b/app/src/main/res/layout/list_item_section_divider.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+        <variable
+            name="ui"
+            type="io.xxlabs.messenger.requests.ui.list.adapter.RequestItem" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingHorizontal="16dp"
+        android:paddingBottom="24dp">
+
+        <View
+            android:id="@+id/divider"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:background="@color/neutral_line"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <TextView
+            android:id="@+id/sectionLabel"
+            style="@style/requests_list_hidden_requests_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:text="@{ui.title}"
+            android:layout_marginTop="24dp"
+            android:textAllCaps="true"
+            android:textColor="@color/neutral_weak"
+            android:textSize="12sp"
+            app:layout_constraintTop_toBottomOf="@id/divider"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            tools:text="Local Connections"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml
index 62a0601f45412d77116d0f640874553ff129819f..b5e8db61fb5d600fffb25c7fa669dfd0c73cd4c4 100644
--- a/app/src/main/res/navigation/nav_main.xml
+++ b/app/src/main/res/navigation/nav_main.xml
@@ -309,9 +309,9 @@
     <!-- TODO: Add a global search action -->
     <fragment
         android:id="@+id/udPrivateSearchFragment"
-        android:name="io.xxlabs.messenger.ui.main.ud.search.UdSearchFragment"
+        android:name="io.xxlabs.messenger.search.UserSearchFragment"
         android:label="UdPrivateSearchFragment"
-        tools:layout="@layout/fragment_private_search">
+        tools:layout="@layout/fragment_user_search">
 
         <action
             android:id="@+id/action_ud_search_to_contact_success"
@@ -320,6 +320,11 @@
             app:exitAnim="@anim/slide_out_left"
             app:popEnterAnim="@anim/slide_in_left"
             app:popExitAnim="@anim/slide_out_right" />
+        <argument
+            android:name="username"
+            app:argType="string"
+            app:nullable="true"
+            android:defaultValue="@null" />
     </fragment>
 
     <fragment
@@ -354,6 +359,15 @@
         android:label="UdProfileFragment"
         tools:layout="@layout/fragment_ud_profile" />
 
+    <action
+        android:id="@+id/action_global_connection_invitation"
+        app:destination="@id/udPrivateSearchFragment"
+        app:enterAnim="@anim/slide_in_right"
+        app:exitAnim="@anim/slide_out_left"
+        app:popEnterAnim="@anim/slide_in_left"
+        app:popExitAnim="@anim/slide_out_right" />
+
+
     <action
         android:id="@+id/action_global_contact_invitation"
         app:destination="@id/contactInvitationFragment"
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index da985b1aff4d14d0b2b483422863680f162eb439..83d57dc0600cbb803c727d77758629f3a9d1dd46 100755
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -194,5 +194,5 @@
     <color name="accent_success">#2CC069</color>
     <color name="accent_safe">#FE7751</color>
 
-    <color name="menu_background">#252525</color>
+    <color name="menu_background">@color/neutral_dark</color>
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2a78a0e24eb3a3c6876d9a24606a8896fedbb1a8..7a8bab9d42bf236571ab91bf2b88acadba3759e3 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -328,7 +328,8 @@
 
     <!-- Search -->
 
-    <string name="search_info_dialog_title">Search</string>
+    <string name="search_title">Search</string>
+    <string name="search_info_dialog_title">@string/search_title</string>
     <string name="search_info_dialog_body">
         You can search for users by their username, email, or phone number using the xx
         network’s Anonymous Data Retrieval protocol which keeps a user’s identity anonymous
@@ -339,8 +340,14 @@
     <string name="search_info_dialog_link_url">https://links.xx.network/adrp</string>
     <string name="search_placeholder_text">
         Your searches are anonymous. Search information is never linked to your account or
-        personally identifiable.</string>
+        personally identifiable. ⓘ</string>
+    <string name="search_placeholder_span">ⓘ</string>
     <string name="search_hint">Search</string>
+    <string name="search_call_to_action">
+        Search for friends anonymously, add them to your connections to start a completely
+        private messaging channel.</string>
+    <string name="search_call_to_action_span_1">friends</string>
+    <string name="search_call_to_action_span_2">connections</string>
 
     <!-- Group Chat -->
 
@@ -626,4 +633,15 @@
     <string name="sftp_login_password_hint">Password</string>
     <string name="sftp_login_port_hint">Port (leave blank for default TCP 22)</string>
     <string name="registration_restore_disabled_error">A username has been submitted. Please reinstall to use restore.</string>
+
+    <string name="share_profile_label">Share profile</string>
+    <string name="share_invitation_message">
+        Hi, I\'m using xx messenger, you can download it here:
+        \nhttps://invite.xx.network
+        \n\nAnd you can add me here:
+        \nhttps://elixxir.io/connect?username=%s
+    </string>
+    <string name="share_chooser_title">@string/share_profile_label</string>
+    <string name="share_no_activity_error">There are no apps installed to share to.</string>
+    <string name="search_generic_error_message">Couldn\'t complete search. Please try again.</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index caee9031cd2c455be60c80cebaf74418deba8f70..f1649964ad00808f7077d686d310c1bdfca635dd 100755
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -371,6 +371,9 @@
         <item name="android:textSize">@dimen/text_14</item>
     </style>
 
+    <style name="SmallTextTabStyle" parent="XxTextStyle">
+        <item name="android:textSize">12sp</item>
+    </style>
 
     <!-- New Styles -->
     <style name="InputLayout" parent="Widget.Design.TextInputLayout">