From 8d3aad457af6ab005452b844d64beeeeb268e4fd Mon Sep 17 00:00:00 2001
From: Ahmed Shehata <ahmed@elixxir.io>
Date: Tue, 7 Jun 2022 21:09:27 +0000
Subject: [PATCH] v1.1.1B142

---
 .../xcschemes/LaunchFeature.xcscheme          |  67 ++++
 .../xcschemes/PushFeature.xcscheme            |  67 ++++
 .../NotificationExtension.entitlements        |   2 +-
 .../NotificationService.swift                 | 120 +------
 App/client-ios.xcodeproj/project.pbxproj      |  30 +-
 .../Resources/client-ios.entitlements         |   2 +-
 Package.swift                                 |  38 +-
 Sources/App/AppDelegate.swift                 |  76 ++--
 Sources/App/DependencyRegistrator.swift       |  53 ++-
 .../Controllers/GroupChatController.swift     |  42 ++-
 .../Controllers/SingleChatController.swift    |  32 +-
 .../ChatFeature/Helpers/BubbleBuilder.swift   |  60 +---
 .../Helpers/CellConfigurator.swift            |   4 -
 .../ViewModels/GroupChatViewModel.swift       |  26 +-
 .../ViewModels/SingleChatViewModel.swift      |  22 +-
 .../Views/Cells/AudioMessageView.swift        |  13 +-
 .../Views/Cells/ImageMessageView.swift        |  19 +-
 .../Views/Cells/ReplyStackMessageView.swift   |  19 +-
 .../Views/Cells/StackMessageView.swift        |  18 +-
 Sources/ChatFeature/Views/LockerView.swift    |  44 ---
 .../Controller/ChatListController.swift       | 338 ++++++++----------
 .../ChatListSearchTableController.swift       | 128 +++++++
 .../Controller/ChatListTableController.swift  | 220 +++++++-----
 .../Coordinator/ChatListCoordinator.swift     |  18 +
 Sources/ChatListFeature/Models/Chat.swift     |  34 ++
 .../ViewModel/ChatListViewModel.swift         | 253 ++++++-------
 .../ChatListFeature/Views/ChatListCell.swift  | 157 ++++----
 .../Views/ChatListContainerView.swift         | 114 ++++++
 .../Views/ChatListEmptyView.swift             |  48 +++
 .../Views/ChatListRecentContactCell.swift     |  89 +++++
 .../Views/ChatListTopLeftNavView.swift        |  72 ++++
 .../Views/ChatListTopRightNavView.swift       |  49 +++
 .../ChatListFeature/Views/ChatListView.swift  | 120 +++----
 .../Views/ChatSearchEmptyView.swift           |  57 +++
 Sources/Database/DB+Contact.swift             |   5 +
 Sources/Database/DB+GroupChatInfo.swift       |   9 +
 Sources/Database/DatabaseManager.swift        |  31 +-
 Sources/Integration/Client.swift              |   2 +-
 Sources/Integration/Extensions.swift          |   3 +-
 .../Implementations/Bindings.swift            |  17 +-
 .../Interfaces/BindingsInterface.swift        |   2 +-
 Sources/Integration/Listeners.swift           |   6 +-
 Sources/Integration/Mocks/BindingsMock.swift  |  20 +-
 .../Integration/Mocks/UserDiscoveryMock.swift |   3 +-
 .../Session/Session+Contacts.swift            |   4 +
 .../Session/Session+Notifications.swift       |   6 +-
 Sources/Integration/Session/Session.swift     |  22 +-
 Sources/Integration/Session/SessionType.swift |   5 +-
 Sources/Integration/XXNetwork.swift           |   2 +-
 Sources/LaunchFeature/LaunchController.swift  | 152 ++++++++
 Sources/LaunchFeature/LaunchCoordinator.swift |  64 ++++
 Sources/LaunchFeature/LaunchView.swift        |  37 ++
 Sources/LaunchFeature/LaunchViewModel.swift   | 190 ++++++++++
 Sources/LaunchFeature/UpdateBlocker.swift     |  24 ++
 Sources/Models/Contact.swift                  |   6 +-
 Sources/Models/GenericChatInfo.swift          |  21 --
 Sources/Models/GroupChatInfo.swift            |   1 +
 .../NetworkMonitor/MockNetworkMonitor.swift   |  17 +-
 .../OnboardingLaunchController.swift          | 165 ---------
 .../Coordinator/OnboardingCoordinator.swift   |   9 -
 .../OnboardingLaunchViewModel.swift           | 152 --------
 Sources/PushFeature/ContentsBuilder.swift     |  23 ++
 Sources/PushFeature/MockPushHandler.swift     |  39 ++
 Sources/PushFeature/Push.swift                |  15 +
 Sources/PushFeature/PushExtractor.swift       |  38 ++
 Sources/PushFeature/PushHandler.swift         | 165 +++++++++
 Sources/PushFeature/PushHandling.swift        |  70 ++++
 Sources/PushFeature/PushRouter.swift          |  22 ++
 Sources/PushFeature/PushType.swift            |  53 +++
 .../PushNotifications/MockPushHandler.swift   |  16 -
 Sources/PushNotifications/PushHandler.swift   | 102 ------
 .../Controllers/RestoreController.swift       |   8 +-
 .../Controllers/RestoreListController.swift   |   6 +-
 .../Views/RestoreListView.swift               |   8 +-
 .../Views/RestoreSuccessView.swift            |   4 +-
 .../RestoreFeature/Views/RestoreView.swift    |  18 +-
 .../Controllers/ScanContainerController.swift |  62 +++-
 .../Controllers/ScanController.swift          |   4 -
 .../Controllers/ScanDisplayController.swift   |  73 ++--
 .../Coordinator/ScanCoordinator.swift         |  40 ++-
 .../ViewModels/ScanDisplayViewModel.swift     |  43 ++-
 .../ViewModels/ScanViewModel.swift            |   3 +-
 .../ScanFeature/Views/AttributeSwitcher.swift | 103 ++++--
 .../ScanFeature/Views/ScanContainerView.swift |  35 +-
 .../Views/ScanDisplayShareView.swift          | 205 ++++++++---
 .../ScanFeature/Views/ScanDisplayView.swift   |  99 +++--
 .../ScanFeature/Views/ScanOverlayView.swift   | 121 +++++--
 Sources/ScanFeature/Views/ScanQRButton.swift  |  59 +++
 Sources/ScanFeature/Views/ScanView.swift      |  58 +--
 .../ScanFeature/Views/SegmentedControl.swift  |  77 ++--
 .../Views/SegmentedControlButton.swift        |  40 ++-
 .../ViewModels/SearchViewModel.swift          |   4 +-
 .../SettingsAdvancedController.swift          |   6 +
 .../SettingsAdvancedViewModel.swift           |  26 ++
 .../ViewModels/SettingsViewModel.swift        |   4 +-
 .../Views/SettingsAdvancedView.swift          |   9 +
 Sources/Shared/AutoGenerated/Assets.swift     |  30 ++
 Sources/Shared/AutoGenerated/Fonts.swift      |   6 +-
 Sources/Shared/AutoGenerated/Strings.swift    | 184 ++++++----
 .../Contents.json                             |  15 +
 .../chat_list_new_group.imageset/Icon.pdf     | 193 ++++++++++
 .../chat_list_ud.imageset/Contents.json       |  15 +
 .../chat_list_ud.imageset/Icon.pdf            | 199 +++++++++++
 .../scan_add.imageset/Contents.json           |  15 +
 .../AssetsScan/scan_add.imageset/Icon.pdf     |  79 ++++
 .../scan_copy.imageset/Contents.json          |  15 +
 .../AssetsScan/scan_copy.imageset/Icon.pdf    | 188 ++++++++++
 .../scan_dropdown.imageset/Contents.json      |  15 +
 .../scan_dropdown.imageset/Icon.pdf           |  73 ++++
 .../AssetsScan/scan_qr.imageset/Contents.json |   3 +-
 .../scan_scan.imageset/Contents.json          |  16 +
 .../AssetsScan/scan_scan.imageset/Icon.pdf    | 111 ++++++
 .../Resources/en.lproj/Localizable.strings    | 153 ++++++--
 Sources/Shared/Views/AvatarView.swift         |   5 +-
 Sources/Shared/Views/SearchComponent.swift    |  52 +--
 Sources/Shared/Views/SnackBar.swift           |  51 ++-
 116 files changed, 4629 insertions(+), 1873 deletions(-)
 create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme
 create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/PushFeature.xcscheme
 delete mode 100644 Sources/ChatFeature/Views/LockerView.swift
 create mode 100644 Sources/ChatListFeature/Controller/ChatListSearchTableController.swift
 create mode 100644 Sources/ChatListFeature/Models/Chat.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatListContainerView.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatListEmptyView.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatListRecentContactCell.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatListTopRightNavView.swift
 create mode 100644 Sources/ChatListFeature/Views/ChatSearchEmptyView.swift
 create mode 100644 Sources/LaunchFeature/LaunchController.swift
 create mode 100644 Sources/LaunchFeature/LaunchCoordinator.swift
 create mode 100644 Sources/LaunchFeature/LaunchView.swift
 create mode 100644 Sources/LaunchFeature/LaunchViewModel.swift
 create mode 100644 Sources/LaunchFeature/UpdateBlocker.swift
 delete mode 100644 Sources/Models/GenericChatInfo.swift
 delete mode 100644 Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift
 delete mode 100644 Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift
 create mode 100644 Sources/PushFeature/ContentsBuilder.swift
 create mode 100644 Sources/PushFeature/MockPushHandler.swift
 create mode 100644 Sources/PushFeature/Push.swift
 create mode 100644 Sources/PushFeature/PushExtractor.swift
 create mode 100644 Sources/PushFeature/PushHandler.swift
 create mode 100644 Sources/PushFeature/PushHandling.swift
 create mode 100644 Sources/PushFeature/PushRouter.swift
 create mode 100644 Sources/PushFeature/PushType.swift
 delete mode 100644 Sources/PushNotifications/MockPushHandler.swift
 delete mode 100644 Sources/PushNotifications/PushHandler.swift
 create mode 100644 Sources/ScanFeature/Views/ScanQRButton.swift
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json
 create mode 100644 Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf

diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme
new file mode 100644
index 00000000..08a24384
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LaunchFeature"
+               BuildableName = "LaunchFeature"
+               BlueprintName = "LaunchFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "LaunchFeature"
+            BuildableName = "LaunchFeature"
+            BlueprintName = "LaunchFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/PushFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/PushFeature.xcscheme
new file mode 100644
index 00000000..2d618948
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/PushFeature.xcscheme
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "PushFeature"
+               BuildableName = "PushFeature"
+               BlueprintName = "PushFeature"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "PushFeature"
+            BuildableName = "PushFeature"
+            BlueprintName = "PushFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/App/NotificationExtension/NotificationExtension.entitlements b/App/NotificationExtension/NotificationExtension.entitlements
index 9d3669c7..f45f1115 100644
--- a/App/NotificationExtension/NotificationExtension.entitlements
+++ b/App/NotificationExtension/NotificationExtension.entitlements
@@ -6,7 +6,7 @@
 	<true/>
 	<key>com.apple.security.application-groups</key>
 	<array>
-		<string>group.io.xxlabs.notification</string>
+		<string>group.elixxir.messenger</string>
 	</array>
 </dict>
 </plist>
diff --git a/App/NotificationExtension/NotificationService.swift b/App/NotificationExtension/NotificationService.swift
index 1c92ce4a..495958b9 100644
--- a/App/NotificationExtension/NotificationService.swift
+++ b/App/NotificationExtension/NotificationService.swift
@@ -1,117 +1,13 @@
-import os
-import Bindings
+import PushFeature
 import UserNotifications
 
-class NotificationService: UNNotificationServiceExtension {
-    var contentHandler: ((UNNotificationContent) -> Void)?
-    var bestAttemptContent: UNMutableNotificationContent?
+final class NotificationService: UNNotificationServiceExtension {
+    private let pushHandler = PushHandler()
 
-    let logger = Logger(subsystem: "logs_xxmessenger", category: "NotificationService.swift")
-    let signpostLogger = OSLog(subsystem: "logs_xxmessenger", category: "NotificationService.swift")
-
-    override func didReceive(_ request: UNNotificationRequest,
-                             withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
-        logger.debug("didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void)")
-
-        self.contentHandler = contentHandler
-        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
-
-        guard let data = bestAttemptContent?.userInfo["notificationData"] as? String,
-              let defaults = UserDefaults(suiteName: "group.io.xxlabs.notification"),
-              let preImage = defaults.value(forKey: "preImage") as? String else {
-                  contentHandler(UNNotificationContent())
-                  logger.error("Failure: No notification data on the payload or no UserDefaults for group.io.xxlabs.notification or no preImage stored.")
-                  return
-              }
-
-        var error: NSError?
-
-        os_signpost(.begin, log: signpostLogger, name: "BindingsNotificationsForMe")
-
-        guard let reports = BindingsNotificationsForMe(data, preImage, &error) else {
-            logger.error("Failure: report list from BindingsNotificationsForMe is nil")
-            os_signpost(.end, log: signpostLogger, name: "BindingsNotificationsForMe")
-            return
-        }
-
-        os_signpost(.end, log: signpostLogger, name: "BindingsNotificationsForMe")
-
-        let length = reports.len()
-        logger.debug("Amount of reports present: \(length, privacy: .public)")
-
-        var showNotification = false
-
-        let content = UNMutableNotificationContent()
-        content.sound = .default
-        content.badge = 1
-        content.threadIdentifier = "new_message_identifier"
-
-        for index in 0..<length {
-            do {
-                let report = try reports.get(index)
-                let isForMe = report.forMe()
-                let isNotDefault = report.type() != "default"
-                let isNotSilent = report.type() != "silent"
-
-                switch report.type() {
-                case "default", "silent":
-                    break
-                case "request":
-                    content.title = "Request received"
-                case "confirm":
-                    content.title = "Request accepted"
-                case "e2e":
-                    content.title = "New private message"
-                case "group":
-                    content.title = "New group message"
-                case "endFT":
-                    content.title = "New media received"
-                case "groupRq":
-                    content.title = "Group request received"
-                case "reset":
-                    content.title = "One of your contacts has restored their account"
-                default:
-                    break
-                }
-
-                logger.log("Type present on the report being iterated: \(report.type(), privacy: .public)")
-
-                if isForMe {
-                    logger.debug("This notification is for me")
-                } else {
-                    logger.debug("This notification is NOT for me")
-                }
-
-                if isForMe && isNotSilent && isNotDefault {
-                    logger.debug("This notification is for me AND its not silent AND is not default -> Will display")
-                    showNotification = true
-                    break
-                } else {
-                    logger.debug("Failure: Its either typed default, silent or its not actually for me")
-                }
-
-            } catch {
-                logger.error("Failure: reports.get raised an exception: \(error.localizedDescription, privacy: .public)")
-            }
-        }
-
-        guard showNotification == true else {
-            logger.debug("Failure: One or more conditions failed. Aborting notification...")
-            return
-        }
-
-        if let error = error {
-            contentHandler(UNNotificationContent())
-            logger.error("Failure: An error was written by NotificationsForMe bindings function: \(error.localizedDescription, privacy: .public)")
-            return
-        }
-
-        contentHandler(content)
-
-        logger.debug("A push was successfully presented")
-    }
-
-    override func serviceExtensionTimeWillExpire() {
-        logger.trace("serviceExtensionTimeWillExpire()")
+    override func didReceive(
+        _ request: UNNotificationRequest,
+        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
+    ) {
+        pushHandler.handlePush(request, contentHandler)
     }
 }
diff --git a/App/client-ios.xcodeproj/project.pbxproj b/App/client-ios.xcodeproj/project.pbxproj
index 3fa6e6ad..9ffb59c7 100644
--- a/App/client-ios.xcodeproj/project.pbxproj
+++ b/App/client-ios.xcodeproj/project.pbxproj
@@ -14,9 +14,9 @@
 		32179BA826410149008B26EC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32179BA726410149008B26EC /* NotificationService.swift */; };
 		32179BAC26410149008B26EC /* NotificationExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 32179BA526410149008B26EC /* NotificationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		3273327126C7391D0027D79D /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 3273327026C7391D0027D79D /* App */; };
-		32824C8F26EAE13D005D3FAC /* Bindings in Frameworks */ = {isa = PBXBuildFile; productRef = 32824C8E26EAE13D005D3FAC /* Bindings */; };
 		32C194E02808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; };
 		32C194E12808C65500876917 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 32C194DF2808C65500876917 /* GoogleService-Info.plist */; };
+		32CAAFAE2845836100446BB9 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 32CAAFAD2845836100446BB9 /* App */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -73,7 +73,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				32824C8F26EAE13D005D3FAC /* Bindings in Frameworks */,
+				32CAAFAE2845836100446BB9 /* App in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -123,9 +123,9 @@
 		32179BA626410149008B26EC /* NotificationExtension */ = {
 			isa = PBXGroup;
 			children = (
-				32DB0549264DD42000FDCCEB /* NotificationExtension.entitlements */,
-				32179BA726410149008B26EC /* NotificationService.swift */,
 				32179BA926410149008B26EC /* Info.plist */,
+				32179BA726410149008B26EC /* NotificationService.swift */,
+				32DB0549264DD42000FDCCEB /* NotificationExtension.entitlements */,
 			);
 			path = NotificationExtension;
 			sourceTree = "<group>";
@@ -179,7 +179,7 @@
 			);
 			name = NotificationExtension;
 			packageProductDependencies = (
-				32824C8E26EAE13D005D3FAC /* Bindings */,
+				32CAAFAD2845836100446BB9 /* App */,
 			);
 			productName = NotificationExtension;
 			productReference = 32179BA526410149008B26EC /* NotificationExtension.appex */;
@@ -448,7 +448,7 @@
 				CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 67;
+				CURRENT_PROJECT_VERSION = 129;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = S6JDM2WW29;
 				ENABLE_BITCODE = NO;
@@ -463,7 +463,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.0.9;
+				MARKETING_VERSION = 1.1.1;
 				OTHER_SWIFT_FLAGS = "$(inherited)";
 				PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -487,7 +487,7 @@
 				CODE_SIGN_ENTITLEMENTS = "client-ios/Resources/client-ios.entitlements";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 67;
+				CURRENT_PROJECT_VERSION = 129;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEVELOPMENT_TEAM = S6JDM2WW29;
 				ENABLE_BITCODE = NO;
@@ -503,7 +503,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 1.0.9;
+				MARKETING_VERSION = 1.1.1;
 				OTHER_SWIFT_FLAGS = "";
 				PRODUCT_BUNDLE_IDENTIFIER = xx.messenger;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -522,7 +522,7 @@
 				CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 67;
+				CURRENT_PROJECT_VERSION = 129;
 				DEVELOPMENT_TEAM = S6JDM2WW29;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -536,7 +536,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 1.0.9;
+				MARKETING_VERSION = 1.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.mock.notifications;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -553,7 +553,7 @@
 				CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 67;
+				CURRENT_PROJECT_VERSION = 129;
 				DEVELOPMENT_TEAM = S6JDM2WW29;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -567,7 +567,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 1.0.9;
+				MARKETING_VERSION = 1.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = xx.messenger.notifications;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
@@ -614,9 +614,9 @@
 			isa = XCSwiftPackageProductDependency;
 			productName = App;
 		};
-		32824C8E26EAE13D005D3FAC /* Bindings */ = {
+		32CAAFAD2845836100446BB9 /* App */ = {
 			isa = XCSwiftPackageProductDependency;
-			productName = Bindings;
+			productName = App;
 		};
 /* End XCSwiftPackageProductDependency section */
 	};
diff --git a/App/client-ios/Resources/client-ios.entitlements b/App/client-ios/Resources/client-ios.entitlements
index ce90eb85..88e46332 100644
--- a/App/client-ios/Resources/client-ios.entitlements
+++ b/App/client-ios/Resources/client-ios.entitlements
@@ -22,7 +22,7 @@
 	<true/>
 	<key>com.apple.security.application-groups</key>
 	<array>
-		<string>group.io.xxlabs.notification</string>
+		<string>group.elixxir.messenger</string>
 	</array>
 </dict>
 </plist>
diff --git a/Package.swift b/Package.swift
index ce66cf78..66b1af7d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -27,9 +27,11 @@ let package = Package(
         .library(name: "MenuFeature", targets: ["MenuFeature"]),
         .library(name: "Integration", targets: ["Integration"]),
         .library(name: "ChatFeature", targets: ["ChatFeature"]),
+        .library(name: "PushFeature", targets: ["PushFeature"]),
         .library(name: "CrashService", targets: ["CrashService"]),
         .library(name: "Presentation", targets: ["Presentation"]),
         .library(name: "BackupFeature", targets: ["BackupFeature"]),
+        .library(name: "LaunchFeature", targets: ["LaunchFeature"]),
         .library(name: "iCloudFeature", targets: ["iCloudFeature"]),
         .library(name: "SearchFeature", targets: ["SearchFeature"]),
         .library(name: "DrawerFeature", targets: ["DrawerFeature"]),
@@ -44,7 +46,6 @@ let package = Package(
         .library(name: "ChatListFeature", targets: ["ChatListFeature"]),
         .library(name: "RequestsFeature", targets: ["RequestsFeature"]),
         .library(name: "ChatInputFeature", targets: ["ChatInputFeature"]),
-        .library(name: "PushNotifications", targets: ["PushNotifications"]),
         .library(name: "OnboardingFeature", targets: ["OnboardingFeature"]),
         .library(name: "GoogleDriveFeature", targets: ["GoogleDriveFeature"]),
         .library(name: "ContactListFeature", targets: ["ContactListFeature"]),
@@ -152,10 +153,12 @@ let package = Package(
                 "ScanFeature",
                 "ChatFeature",
                 "MenuFeature",
+                "PushFeature",
                 "ToastFeature",
                 "CrashService",
                 "BackupFeature",
                 "SearchFeature",
+                "LaunchFeature",
                 "iCloudFeature",
                 "DropboxFeature",
                 "ContactFeature",
@@ -165,7 +168,6 @@ let package = Package(
                 "ChatListFeature",
                 "SettingsFeature",
                 "RequestsFeature",
-                "PushNotifications",
                 "OnboardingFeature",
                 "GoogleDriveFeature",
                 "ContactListFeature"
@@ -178,7 +180,7 @@ let package = Package(
         .target(name: "InputField", dependencies: ["Shared"]),
         .binaryTarget(name: "Bindings", path: "XCFrameworks/Bindings.xcframework"),
 
-        // MARK: - PushNotifications
+        // MARK: - Permissions
 
             .target(
                 name: "Permissions",
@@ -189,10 +191,13 @@ let package = Package(
                 ]
             ),
 
+        // MARK: - PushFeature
+
             .target(
-                name: "PushNotifications",
+                name: "PushFeature",
                 dependencies: [
-                    "XXLogger",
+                    "Models",
+                    "Database",
                     "Defaults",
                     "Integration",
                     "DependencyInjection"
@@ -430,6 +435,7 @@ let package = Package(
                     "Shared",
                     "Database",
                     "Bindings",
+                    "ToastFeature",
                     "BackupFeature",
                     "CrashReporting",
                     "NetworkMonitor",
@@ -552,6 +558,24 @@ let package = Package(
                 ]
             ),
 
+        // MARK: - LaunchFeature
+
+            .target(
+                name: "LaunchFeature",
+                dependencies: [
+                    "HUD",
+                    "Theme",
+                    "Shared",
+                    "Defaults",
+                    "PushFeature",
+                    "Integration",
+                    "Permissions",
+                    "DropboxFeature",
+                    "VersionChecking",
+                    "DependencyInjection"
+                ]
+            ),
+
         // MARK: - RequestsFeature
 
             .target(
@@ -632,11 +656,11 @@ let package = Package(
                     "Countries",
                     "InputField",
                     "Permissions",
+                    "PushFeature",
                     "Integration",
                     "Presentation",
                     "DrawerFeature",
                     "VersionChecking",
-                    "PushNotifications",
                     "DependencyInjection",
                     .product(
                         name: "ScrollViewController",
@@ -729,12 +753,12 @@ let package = Package(
                     "Defaults",
                     "Keychain",
                     "InputField",
+                    "PushFeature",
                     "Permissions",
                     "MenuFeature",
                     "Integration",
                     "Presentation",
                     "DrawerFeature",
-                    "PushNotifications",
                     "DependencyInjection",
                     .product(
                         name: "ScrollViewController",
diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift
index b6319b9c..ecb65d1f 100644
--- a/Sources/App/AppDelegate.swift
+++ b/Sources/App/AppDelegate.swift
@@ -1,23 +1,20 @@
 import UIKit
-import OSLog
 import BackgroundTasks
 
 import Theme
 import XXLogger
 import Defaults
 import Integration
+import PushFeature
 import ToastFeature
 import SwiftyDropbox
+import LaunchFeature
+import DropboxFeature
 import CrashReporting
-import PushNotifications
 import DependencyInjection
 
-import OnboardingFeature
-import DropboxFeature
-
-let logger = Logger(subsystem: "logs_xxmessenger", category: "AppDelegate.swift")
-
 public class AppDelegate: UIResponder, UIApplicationDelegate {
+    @Dependency private var pushRouter: PushRouter
     @Dependency private var pushHandler: PushHandling
     @Dependency private var crashReporter: CrashReporter
     @Dependency private var dropboxService: DropboxInterface
@@ -52,21 +49,17 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
 
         UNUserNotificationCenter.current().delegate = self
 
-        let rootScreen =
-        StatusBarViewController(
-            ToastViewController(
-                UINavigationController(
-                    rootViewController: OnboardingLaunchController()
-                )
-            )
-        )
+        let window = Window()
+        let navController = UINavigationController(rootViewController: LaunchController())
+        window.rootViewController = StatusBarViewController(ToastViewController(navController))
+        window.backgroundColor = UIColor.white
+        window.makeKeyAndVisible()
+        self.window = window
 
-        window = Window()
-        window?.rootViewController = rootScreen
-        window?.backgroundColor = UIColor.white
-        window?.makeKeyAndVisible()
+        DependencyInjection.Container.shared.register(
+            PushRouter.live(navigationController: navController)
+        )
 
-        UserDefaults.standard.set(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
         return true
     }
 
@@ -76,25 +69,19 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
 
     public func applicationDidEnterBackground(_ application: UIApplication) {
         if let session = try? DependencyInjection.Container.shared.resolve() as SessionType {
-            let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {
-                logger.log("Background task will expire")
-            }
+            let backgroundTask = application.beginBackgroundTask(withName: "xx.stop.network") {}
 
             // An option here would be: create async completion closure
 
             backgroundTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
-                logger.log("Background time remaining: \(UIApplication.shared.backgroundTimeRemaining)")
-
                 guard UIApplication.shared.backgroundTimeRemaining > 8 else {
                     if !self.calledStopNetwork {
                         self.calledStopNetwork = true
                         session.stop()
-                        logger.log("Stopping client threads...")
                     } else {
                         if session.hasRunningTasks == false {
                             application.endBackgroundTask(backgroundTask)
                             timer.invalidate()
-                            logger.log("Finished background processes")
                         }
                     }
 
@@ -104,10 +91,7 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
                 guard UIApplication.shared.backgroundTimeRemaining > 9 else {
                     if !self.forceFailedPendingMessages {
                         self.forceFailedPendingMessages = true
-                        logger.log("Background time is running out. Will force-fail all pending messages")
                         session.forceFailMessages()
-                    } else {
-                        logger.log("Background time is running out without pending messages")
                     }
 
                     return
@@ -127,27 +111,18 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
     
     public func applicationWillTerminate(_ application: UIApplication) {
         if let session = try? DependencyInjection.Container.shared.resolve() as SessionType {
-            logger.log("applicationWillTerminate but has an ongoing session. Calling stopNetwork...")
             session.stop()
-        } else {
-            logger.log("applicationWillTerminate without any session")
         }
     }
 
     public func applicationWillEnterForeground(_ application: UIApplication) {
         if backgroundTimer != nil {
-            logger.log("Invalidating background timer...")
             backgroundTimer?.invalidate()
             backgroundTimer = nil
         }
 
         if let session = try? DependencyInjection.Container.shared.resolve() as SessionType {
-            guard self.calledStopNetwork == true else {
-                logger.log("A client instance is already running. Moving on...")
-                return
-            }
-
-            logger.log("A client instance is stopped. Starting network...")
+            guard self.calledStopNetwork == true else { return }
             session.start()
             self.calledStopNetwork = false
         }
@@ -158,7 +133,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
         coverView?.removeFromSuperview()
     }
 
-    public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+    public func application(
+        _ app: UIApplication,
+        open url: URL,
+        options: [UIApplication.OpenURLOptionsKey : Any] = [:]
+    ) -> Bool {
         dropboxService.handleOpenUrl(url)
     }
 }
@@ -166,18 +145,27 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
 // MARK: Notifications
 
 extension AppDelegate: UNUserNotificationCenterDelegate {
+    public func userNotificationCenter(
+        _ center: UNUserNotificationCenter,
+        didReceive response: UNNotificationResponse,
+        withCompletionHandler completionHandler: @escaping () -> Void
+    ) {
+        let userInfo = response.notification.request.content.userInfo
+        pushHandler.handleAction(pushRouter, userInfo, completionHandler)
+    }
+
     public func application(
-        _: UIApplication,
+        _ application: UIApplication,
         didReceiveRemoteNotification notification: [AnyHashable: Any],
         fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
     ) {
-        pushHandler.didReceiveRemote(notification, completionHandler)
+        pushHandler.handlePush(notification, completionHandler)
     }
 
     public func application(
         _: UIApplication,
         didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
     ) {
-        pushHandler.didRegisterWith(deviceToken)
+        pushHandler.registerToken(deviceToken)
     }
 }
diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift
index 5d87fd00..b342330e 100644
--- a/Sources/App/DependencyRegistrator.swift
+++ b/Sources/App/DependencyRegistrator.swift
@@ -17,6 +17,7 @@ import Countries
 import Voxophone
 import Integration
 import Permissions
+import PushFeature
 import CrashService
 import ToastFeature
 import iCloudFeature
@@ -24,7 +25,6 @@ import CrashReporting
 import NetworkMonitor
 import DropboxFeature
 import VersionChecking
-import PushNotifications
 import GoogleDriveFeature
 import DependencyInjection
 
@@ -35,6 +35,7 @@ import ChatFeature
 import MenuFeature
 import BackupFeature
 import SearchFeature
+import LaunchFeature
 import RestoreFeature
 import ContactFeature
 import ProfileFeature
@@ -107,6 +108,15 @@ struct DependencyRegistrator {
 
         // MARK: Coordinators
 
+        container.register(
+            LaunchCoordinator(
+                requestsFactory: RequestsContainerController.init,
+                chatListFactory: ChatListController.init,
+                onboardingFactory: OnboardingStartController.init(_:),
+                singleChatFactory: SingleChatController.init(_:),
+                groupChatFactory: GroupChatController.init(_:)
+            ) as LaunchCoordinating)
+
         container.register(
             BackupCoordinator(
                 passphraseFactory: BackupPassphraseController.init(_:_:)
@@ -190,7 +200,6 @@ struct DependencyRegistrator {
                 searchFactory: SearchController.init,
                 welcomeFactory: OnboardingWelcomeController.init,
                 chatListFactory: ChatListController.init,
-                startFactory: OnboardingStartController.init(_:),
                 usernameFactory: OnboardingUsernameController.init(_:),
                 restoreListFactory: RestoreListController.init(_:),
                 successFactory: OnboardingSuccessController.init(_:),
@@ -214,20 +223,60 @@ struct DependencyRegistrator {
 
         container.register(
             ScanCoordinator(
+                emailFactory: ProfileEmailController.init,
+                phoneFactory: ProfilePhoneController.init,
                 contactsFactory: ContactListController.init,
                 requestsFactory: RequestsContainerController.init,
                 contactFactory: ContactController.init(_:),
                 sideMenuFactory: MenuController.init(_:_:)
             ) as ScanCoordinating)
 
+
         container.register(
             ChatListCoordinator(
                 scanFactory: ScanContainerController.init,
                 searchFactory: SearchController.init,
+                newGroupFactory: CreateGroupController.init,
                 contactsFactory: ContactListController.init,
+                contactFactory: ContactController.init(_:),
                 singleChatFactory: SingleChatController.init(_:),
                 groupChatFactory: GroupChatController.init(_:),
                 sideMenuFactory: MenuController.init(_:_:)
             ) as ChatListCoordinating)
     }
 }
+
+extension PushRouter {
+    static func live(navigationController: UINavigationController) -> PushRouter {
+        PushRouter { route, completion in
+            if let launchController = navigationController.viewControllers.last as? LaunchController {
+                launchController.pendingPushRoute = route
+            } else {
+                switch route {
+                case .requests:
+                    if (navigationController.viewControllers.last as? RequestsContainerController) == nil {
+                        navigationController.setViewControllers([RequestsContainerController()], animated: true)
+                    }
+                case .contactChat(id: let id):
+                    if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
+                       let contact = session.getContactWith(userId: id) {
+                        navigationController.setViewControllers([
+                            ChatListController(),
+                            SingleChatController(contact)
+                        ], animated: true)
+                    }
+                case .groupChat(id: let id):
+                    if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
+                       let info = session.getGroupChatInfoWith(groupId: id) {
+                        navigationController.setViewControllers([
+                            ChatListController(),
+                            GroupChatController(info)
+                        ], animated: true)
+                    }
+                }
+            }
+
+            completion()
+        }
+    }
+}
diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift
index 174bb5fa..e248c053 100644
--- a/Sources/ChatFeature/Controllers/GroupChatController.swift
+++ b/Sources/ChatFeature/Controllers/GroupChatController.swift
@@ -5,6 +5,7 @@ import Shared
 import Combine
 import Voxophone
 import ChatLayout
+import DrawerFeature
 import DifferenceKit
 import ChatInputFeature
 import DependencyInjection
@@ -30,6 +31,7 @@ public final class GroupChatController: UIViewController {
     private let viewModel: GroupChatViewModel
     private let layoutDelegate = LayoutDelegate()
     private var cancellables = Set<AnyCancellable>()
+    private var drawerCancellables = Set<AnyCancellable>()
     private var sections = [ArraySection<ChatSection, GroupChatItem>]()
     private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>()
 
@@ -157,10 +159,16 @@ public final class GroupChatController: UIViewController {
     }
 
     private func setupBindings() {
-        viewModel.roundURLPublisher
+        viewModel.routesPublisher
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toWebview(with: $0, from: self) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                switch $0 {
+                case .waitingRound:
+                    coordinator.toDrawer(makeWaitingRoundDrawer(), from: self)
+                case .webview(let urlString):
+                    coordinator.toWebview(with: urlString, from: self)
+                }
+            }.store(in: &cancellables)
 
         viewModel.messages
             .receive(on: DispatchQueue.main)
@@ -219,6 +227,34 @@ public final class GroupChatController: UIViewController {
         coordinator.toMembersList(members, from: self)
     }
 
+    private func makeWaitingRoundDrawer() -> UIViewController {
+        let text = DrawerText(
+            font: Fonts.Mulish.semiBold.font(size: 14.0),
+            text: Localized.Chat.RoundDrawer.title,
+            color: Asset.neutralWeak.color,
+            lineHeightMultiple: 1.35,
+            spacingAfter: 25
+        )
+
+        let button = DrawerCapsuleButton(model: .init(
+            title: Localized.Chat.RoundDrawer.action,
+            style: .brandColored
+        ))
+
+        let drawer = DrawerController(with: [text, button])
+
+        button.action
+            .receive(on: DispatchQueue.main)
+            .sink { [weak drawer] in
+                drawer?.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
+                }
+            }.store(in: &drawerCancellables)
+
+        return drawer
+    }
+
     func scrollToBottom(completion: (() -> Void)? = nil) {
         let contentOffsetAtBottom = CGPoint(
             x: collectionView.contentOffset.x,
diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift
index 41526deb..b6fd74a1 100644
--- a/Sources/ChatFeature/Controllers/SingleChatController.swift
+++ b/Sources/ChatFeature/Controllers/SingleChatController.swift
@@ -25,7 +25,6 @@ public final class SingleChatController: UIViewController {
     @Dependency private var coordinator: ChatCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
-
     lazy private var infoView = UIControl()
     lazy private var nameLabel = UILabel()
     lazy private var avatarView = AvatarView()
@@ -89,6 +88,7 @@ public final class SingleChatController: UIViewController {
         super.viewDidAppear(animated)
         collectionView.collectionViewLayout.invalidateLayout()
         becomeFirstResponder()
+        viewModel.viewDidAppear()
     }
 
     private var isFirstAppearance = true
@@ -223,6 +223,8 @@ public final class SingleChatController: UIViewController {
                     coordinator.toPermission(type: .library, from: self)
                 case .webview(let urlString):
                     coordinator.toWebview(with: urlString, from: self)
+                case .waitingRound:
+                    coordinator.toDrawer(makeWaitingRoundDrawer(), from: self)
                 case .none:
                     break
                 }
@@ -351,6 +353,34 @@ public final class SingleChatController: UIViewController {
         }
     }
 
+    private func makeWaitingRoundDrawer() -> UIViewController {
+        let text = DrawerText(
+            font: Fonts.Mulish.semiBold.font(size: 14.0),
+            text: Localized.Chat.RoundDrawer.title,
+            color: Asset.neutralWeak.color,
+            lineHeightMultiple: 1.35,
+            spacingAfter: 25
+        )
+
+        let button = DrawerCapsuleButton(model: .init(
+            title: Localized.Chat.RoundDrawer.action,
+            style: .brandColored
+        ))
+
+        let drawer = DrawerController(with: [text, button])
+
+        button.action
+            .receive(on: DispatchQueue.main)
+            .sink { [weak drawer] in
+                drawer?.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
+                }
+            }.store(in: &drawerCancellables)
+
+        return drawer
+    }
+
     private func presentDeleteAllDrawer() {
         let clearButton = CapsuleButton()
         clearButton.setStyle(.red)
diff --git a/Sources/ChatFeature/Helpers/BubbleBuilder.swift b/Sources/ChatFeature/Helpers/BubbleBuilder.swift
index e770bf23..3f9da31d 100644
--- a/Sources/ChatFeature/Helpers/BubbleBuilder.swift
+++ b/Sources/ChatFeature/Helpers/BubbleBuilder.swift
@@ -10,32 +10,27 @@ final class Bubbler {
 
         switch item.status {
         case .received, .read:
-            audioBubble.lockerView.removeFromSuperview()
+            audioBubble.lockerImageView.removeFromSuperview()
             audioBubble.backgroundColor = Asset.neutralWhite.color
             audioBubble.dateLabel.textColor = Asset.neutralDisabled.color
             audioBubble.progressLabel.textColor = Asset.neutralDisabled.color
         case .receivingAttachment:
-            audioBubble.lockerView.animate()
             audioBubble.backgroundColor = Asset.neutralWhite.color
             audioBubble.dateLabel.textColor = Asset.neutralDisabled.color
             audioBubble.progressLabel.textColor = Asset.neutralDisabled.color
         case .timedOut:
-            audioBubble.lockerView.fail()
             audioBubble.backgroundColor = Asset.accentWarning.color
             audioBubble.dateLabel.textColor = Asset.neutralWhite.color
             audioBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .failedToSend:
-            audioBubble.lockerView.fail()
             audioBubble.backgroundColor = Asset.accentDanger.color
             audioBubble.dateLabel.textColor = Asset.neutralWhite.color
             audioBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .sent:
-            audioBubble.lockerView.stop()
             audioBubble.backgroundColor = Asset.brandBubble.color
             audioBubble.dateLabel.textColor = Asset.neutralWhite.color
             audioBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .sending, .sendingAttachment:
-            audioBubble.lockerView.animate()
             audioBubble.backgroundColor = Asset.brandBubble.color
             audioBubble.dateLabel.textColor = Asset.neutralWhite.color
             audioBubble.progressLabel.textColor = Asset.neutralWhite.color
@@ -52,32 +47,27 @@ final class Bubbler {
 
         switch item.status {
         case .received, .read:
-            imageBubble.lockerView.removeFromSuperview()
+            imageBubble.lockerImageView.removeFromSuperview()
             imageBubble.backgroundColor = Asset.neutralWhite.color
             imageBubble.dateLabel.textColor = Asset.neutralDisabled.color
             imageBubble.progressLabel.textColor = Asset.neutralDisabled.color
         case .receivingAttachment:
-            imageBubble.lockerView.animate()
             imageBubble.backgroundColor = Asset.neutralWhite.color
             imageBubble.dateLabel.textColor = Asset.neutralDisabled.color
             imageBubble.progressLabel.textColor = Asset.neutralDisabled.color
         case .failedToSend:
-            imageBubble.lockerView.fail()
             imageBubble.backgroundColor = Asset.accentDanger.color
             imageBubble.dateLabel.textColor = Asset.neutralWhite.color
             imageBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .timedOut:
-            imageBubble.lockerView.fail()
             imageBubble.backgroundColor = Asset.accentWarning.color
             imageBubble.dateLabel.textColor = Asset.neutralWhite.color
             imageBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .sent:
-            imageBubble.lockerView.stop()
             imageBubble.backgroundColor = Asset.brandBubble.color
             imageBubble.dateLabel.textColor = Asset.neutralWhite.color
             imageBubble.progressLabel.textColor = Asset.neutralWhite.color
         case .sending, .sendingAttachment:
-            imageBubble.lockerView.animate()
             imageBubble.backgroundColor = Asset.brandBubble.color
             imageBubble.dateLabel.textColor = Asset.neutralWhite.color
             imageBubble.progressLabel.textColor = Asset.neutralWhite.color
@@ -96,32 +86,28 @@ final class Bubbler {
 
         switch item.status {
         case .received, .read, .receivingAttachment:
-            bubble.lockerView.removeFromSuperview()
+            bubble.lockerImageView.removeFromSuperview()
             bubble.backgroundColor = Asset.neutralWhite.color
             bubble.textView.textColor = Asset.neutralActive.color
             bubble.dateLabel.textColor = Asset.neutralDisabled.color
             roundButtonColor = Asset.neutralDisabled.color
             bubble.revertBottomStackOrder()
         case .timedOut:
-            bubble.lockerView.fail()
             bubble.backgroundColor = Asset.accentWarning.color
             bubble.textView.textColor = Asset.neutralWhite.color
             bubble.dateLabel.textColor = Asset.neutralWhite.color
             roundButtonColor = Asset.neutralWhite.color
         case .failedToSend:
-            bubble.lockerView.fail()
             bubble.backgroundColor = Asset.accentDanger.color
             bubble.textView.textColor = Asset.neutralWhite.color
             bubble.dateLabel.textColor = Asset.neutralWhite.color
             roundButtonColor = Asset.neutralWhite.color
         case .sent:
-            bubble.lockerView.stop()
             bubble.backgroundColor = Asset.brandBubble.color
             bubble.textView.textColor = Asset.neutralWhite.color
             bubble.dateLabel.textColor = Asset.neutralWhite.color
             roundButtonColor = Asset.neutralWhite.color
         case .sending, .sendingAttachment:
-            bubble.lockerView.animate()
             bubble.backgroundColor = Asset.brandBubble.color
             bubble.textView.textColor = Asset.neutralWhite.color
             bubble.dateLabel.textColor = Asset.neutralWhite.color
@@ -137,8 +123,8 @@ final class Bubbler {
                 .font: Fonts.Mulish.regular.font(size: 12.0) as Any
             ]
         )
+
         bubble.roundButton.setAttributedTitle(attrString, for: .normal)
-        bubble.roundButton.isHidden = item.roundURL == nil
     }
 
     static func buildGroup(
@@ -158,7 +144,7 @@ final class Bubbler {
             bubble.textView.textColor = Asset.neutralActive.color
             bubble.dateLabel.textColor = Asset.neutralDisabled.color
             roundButtonColor = Asset.neutralDisabled.color
-            bubble.lockerView.removeFromSuperview()
+            bubble.lockerImageView.removeFromSuperview()
             bubble.revertBottomStackOrder()
         case .failed:
             bubble.senderLabel.removeFromSuperview()
@@ -185,18 +171,6 @@ final class Bubbler {
         )
 
         bubble.roundButton.setAttributedTitle(attrString, for: .normal)
-        bubble.roundButton.isHidden = item.roundURL == nil
-
-        switch item.status {
-        case .sent:
-            bubble.lockerView.stop()
-        case .failed:
-            bubble.lockerView.fail()
-        case .sending:
-            bubble.lockerView.animate()
-        case .read, .received:
-            bubble.lockerView.removeFromSuperview()
-        }
     }
 
     static func buildReply(
@@ -258,18 +232,6 @@ final class Bubbler {
             ]
         )
         bubble.roundButton.setAttributedTitle(attrString, for: .normal)
-        bubble.roundButton.isHidden = item.roundURL == nil
-
-        switch item.status {
-        case .sent:
-            bubble.lockerView.stop()
-        case .failedToSend, .timedOut:
-            bubble.lockerView.fail()
-        case .sending, .sendingAttachment:
-            bubble.lockerView.animate()
-        case .read, .received, .receivingAttachment:
-            bubble.lockerView.removeFromSuperview()
-        }
     }
 
     static func buildReplyGroup(
@@ -295,7 +257,7 @@ final class Bubbler {
             roundButtonColor = Asset.neutralDisabled.color
             bubble.replyView.container.backgroundColor = Asset.brandDefault.color
             bubble.replyView.space.backgroundColor = Asset.brandPrimary.color
-            bubble.lockerView.removeFromSuperview()
+            bubble.lockerImageView.removeFromSuperview()
             bubble.revertBottomStackOrder()
         case .failed:
             bubble.senderLabel.removeFromSuperview()
@@ -326,15 +288,5 @@ final class Bubbler {
         )
 
         bubble.roundButton.setAttributedTitle(attrString, for: .normal)
-        bubble.roundButton.isHidden = item.roundURL == nil
-
-        switch item.status {
-        case .failed:
-            bubble.lockerView.fail()
-        case .sent:
-            bubble.lockerView.stop()
-        default:
-            bubble.lockerView.animate()
-        }
     }
 }
diff --git a/Sources/ChatFeature/Helpers/CellConfigurator.swift b/Sources/ChatFeature/Helpers/CellConfigurator.swift
index 75ac2ce6..5f2cb1b6 100644
--- a/Sources/ChatFeature/Helpers/CellConfigurator.swift
+++ b/Sources/ChatFeature/Helpers/CellConfigurator.swift
@@ -258,7 +258,6 @@ extension CellFactory {
 
                 cell.canReply = item.status.canReply
                 cell.performReply = performReply
-                cell.rightView.roundButton.isHidden = item.roundURL == nil
                 cell.rightView.didTapShowRound = { showRound(item.roundURL) }
                 return cell
             }
@@ -290,7 +289,6 @@ extension CellFactory {
                 )
                 cell.canReply = item.status.canReply
                 cell.performReply = performReply
-                cell.leftView.roundButton.isHidden = item.roundURL == nil
                 cell.leftView.didTapShowRound = { showRound(item.roundURL) }
                 cell.leftView.revertBottomStackOrder()
                 return cell
@@ -346,7 +344,6 @@ extension CellFactory {
                 Bubbler.build(bubble: cell.leftView, with: item)
                 cell.canReply = item.status.canReply
                 cell.performReply = performReply
-                cell.leftView.roundButton.isHidden = item.roundURL == nil
                 cell.leftView.didTapShowRound = { showRound(item.roundURL) }
                 cell.leftView.revertBottomStackOrder()
                 return cell
@@ -370,7 +367,6 @@ extension CellFactory {
                 Bubbler.build(bubble: cell.rightView, with: item)
                 cell.canReply = item.status.canReply
                 cell.performReply = performReply
-                cell.rightView.roundButton.isHidden = item.roundURL == nil
                 cell.rightView.didTapShowRound = { showRound(item.roundURL) }
 
                 return cell
diff --git a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift
index 37504382..ad29c602 100644
--- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift
+++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift
@@ -6,18 +6,27 @@ import Integration
 import DifferenceKit
 import DependencyInjection
 
+enum GroupChatNavigationRoutes: Equatable {
+    case waitingRound
+    case webview(String)
+}
+
 final class GroupChatViewModel {
     @Dependency private var session: SessionType
 
+    var replyPublisher: AnyPublisher<ReplyModel, Never> {
+        replySubject.eraseToAnyPublisher()
+    }
+
+    var routesPublisher: AnyPublisher<GroupChatNavigationRoutes, Never> {
+        routesSubject.eraseToAnyPublisher()
+    }
+
     let info: GroupChatInfo
     private var stagedReply: Reply?
     private var cancellables = Set<AnyCancellable>()
     private let replySubject = PassthroughSubject<ReplyModel, Never>()
-
-    var roundURLPublisher: AnyPublisher<String, Never> { roundURLSubject.eraseToAnyPublisher() }
-    private let roundURLSubject = PassthroughSubject<String, Never>()
-
-    var replyPublisher: AnyPublisher<ReplyModel, Never> { replySubject.eraseToAnyPublisher() }
+    private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>()
 
     var messages: AnyPublisher<[ArraySection<ChatSection, GroupChatItem>], Never> {
         session.groupMessages(info.group)
@@ -65,8 +74,11 @@ final class GroupChatViewModel {
     }
 
     func showRoundFrom(_ roundURL: String?) {
-        guard let urlString = roundURL else { return }
-        roundURLSubject.send(urlString)
+        if let urlString = roundURL, !urlString.isEmpty {
+            routesSubject.send(.webview(urlString))
+        } else {
+            routesSubject.send(.waitingRound)
+        }
     }
 
     func abortReply() {
diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
index 3ec0712e..d1be4b6e 100644
--- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
+++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift
@@ -19,6 +19,7 @@ enum SingleChatNavigationRoutes: Equatable {
     case none
     case camera
     case library
+    case waitingRound
     case cameraPermission
     case libraryPermission
     case microphonePermission
@@ -55,9 +56,23 @@ final class SingleChatViewModel {
         }.eraseToAnyPublisher()
     }
 
+    private func updateRecentState(_ contact: Contact) {
+        if contact.isRecent == true {
+            var contact = contact
+            contact.isRecent = false
+            session.update(contact)
+        }
+    }
+
+    func viewDidAppear() {
+        updateRecentState(contact)
+    }
+
     init(_ contact: Contact) {
         self.contactSubject = .init(contact)
 
+        updateRecentState(contact)
+
         session.contacts(.withUserId(contact.userId))
             .compactMap { $0.first }
             .sink { [unowned self] in contactSubject.send($0) }
@@ -180,8 +195,11 @@ final class SingleChatViewModel {
     }
 
     func showRoundFrom(_ roundURL: String?) {
-        guard let urlString = roundURL else { return }
-        navigationRoutes.send(.webview(urlString))
+        if let urlString = roundURL, !urlString.isEmpty {
+            navigationRoutes.send(.webview(urlString))
+        } else {
+            navigationRoutes.send(.waitingRound)
+        }
     }
 
     func didRequestDelete(_ items: [ChatItem]) {
diff --git a/Sources/ChatFeature/Views/Cells/AudioMessageView.swift b/Sources/ChatFeature/Views/Cells/AudioMessageView.swift
index 38979a14..5982ffc4 100644
--- a/Sources/ChatFeature/Views/Cells/AudioMessageView.swift
+++ b/Sources/ChatFeature/Views/Cells/AudioMessageView.swift
@@ -16,13 +16,14 @@ struct AudioMessageCellState {
 }
 
 final class AudioMessageView: UIView, CollectionCellContent {
-    private(set) var progressLabel = UILabel()
-    private(set) var dateLabel = UILabel()
     private let playerView = AudioView()
     private let stackView = UIStackView()
     private let shapeLayer = CAShapeLayer()
     private let bottomStack = UIStackView()
-    private(set) var lockerView = LockerView()
+
+    private(set) var dateLabel = UILabel()
+    private(set) var progressLabel = UILabel()
+    private(set) var lockerImageView = UIImageView()
 
     var didTapLeft: (() -> Void)?
     var didTapRight: (() -> Void)?
@@ -46,7 +47,6 @@ final class AudioMessageView: UIView, CollectionCellContent {
         dateLabel.text = nil
         progressLabel.text = nil
         playerView.timeLabel.text = nil
-        lockerView.icon.layer.removeAllAnimations()
         cancellables.removeAll()
     }
 
@@ -77,6 +77,9 @@ final class AudioMessageView: UIView, CollectionCellContent {
     }
 
     private func setup() {
+        lockerImageView.contentMode = .center
+        lockerImageView.image = Asset.chatLocker.image
+
         dateLabel.textColor = Asset.neutralWhite.color
         dateLabel.font = Fonts.Mulish.regular.font(size: 12.0)
         progressLabel.textColor = Asset.neutralWhite.color
@@ -86,7 +89,7 @@ final class AudioMessageView: UIView, CollectionCellContent {
         bottomStack.spacing = 10
         bottomStack.addArrangedSubview(progressLabel.pinning(at: .left(0)))
         bottomStack.addArrangedSubview(dateLabel.pinning(at: .right(0)))
-        bottomStack.addArrangedSubview(lockerView)
+        bottomStack.addArrangedSubview(lockerImageView)
 
         stackView.axis = .vertical
         stackView.addArrangedSubview(playerView)
diff --git a/Sources/ChatFeature/Views/Cells/ImageMessageView.swift b/Sources/ChatFeature/Views/Cells/ImageMessageView.swift
index a7564129..88efa910 100644
--- a/Sources/ChatFeature/Views/Cells/ImageMessageView.swift
+++ b/Sources/ChatFeature/Views/Cells/ImageMessageView.swift
@@ -5,17 +5,14 @@ typealias OutgoingImageCell = CollectionCell<FlexibleSpace, ImageMessageView>
 typealias IncomingImageCell = CollectionCell<ImageMessageView, FlexibleSpace>
 
 final class ImageMessageView: UIView, CollectionCellContent {
-    // MARK: UI
-
-    let dateLabel = UILabel()
-    let progressLabel = UILabel()
-    let imageView = UIImageView()
-    let lockerView = LockerView()
     private let stackView = UIStackView()
     private let shapeLayer = CAShapeLayer()
     private let bottomStack = UIStackView()
 
-    // MARK: Lifecycle
+    private(set) var dateLabel = UILabel()
+    private(set) var progressLabel = UILabel()
+    private(set) var imageView = UIImageView()
+    private(set) var lockerImageView = UIImageView()
 
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -33,12 +30,12 @@ final class ImageMessageView: UIView, CollectionCellContent {
         imageView.image = nil
         dateLabel.text = nil
         progressLabel.text = nil
-        lockerView.icon.layer.removeAllAnimations()
     }
 
-    // MARK: Private
-
     private func setup() {
+        lockerImageView.contentMode = .center
+        lockerImageView.image = Asset.chatLocker.image
+
         imageView.layer.cornerRadius = 10
 
         dateLabel.font = Fonts.Mulish.regular.font(size: 12.0)
@@ -48,7 +45,7 @@ final class ImageMessageView: UIView, CollectionCellContent {
         bottomStack.spacing = 10
         bottomStack.addArrangedSubview(progressLabel.pinning(at: .left(0)))
         bottomStack.addArrangedSubview(dateLabel.pinning(at: .right(0)))
-        bottomStack.addArrangedSubview(lockerView)
+        bottomStack.addArrangedSubview(lockerImageView)
 
         stackView.axis = .vertical
         stackView.spacing = 5
diff --git a/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift b/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift
index 594f9f88..20a74276 100644
--- a/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift
+++ b/Sources/ChatFeature/Views/Cells/ReplyStackMessageView.swift
@@ -6,16 +6,17 @@ typealias OutgoingReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageVie
 typealias OutgoingFailedReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView>
 
 final class ReplyStackMessageView: UIView, CollectionCellContent {
-    let roundButton = UIButton()
-    let dateLabel = UILabel()
-    let textView = TextView()
-    let replyView = ReplyView()
-    let lockerView = LockerView()
-    let senderLabel = UILabel()
     private let stackView = UIStackView()
     private let shapeLayer = CAShapeLayer()
     private let bottomStack = UIStackView()
 
+    private(set) var dateLabel = UILabel()
+    private(set) var textView = TextView()
+    private(set) var replyView = ReplyView()
+    private(set) var senderLabel = UILabel()
+    private(set) var roundButton = UIButton()
+    private(set) var lockerImageView = UIImageView()
+
     var didTapShowRound: (() -> Void)?
 
     override init(frame: CGRect) {
@@ -36,7 +37,6 @@ final class ReplyStackMessageView: UIView, CollectionCellContent {
         replyView.cleanUp()
         senderLabel.text = nil
         textView.resignFirstResponder()
-        lockerView.icon.layer.removeAllAnimations()
         didTapShowRound = nil
     }
 
@@ -46,6 +46,9 @@ final class ReplyStackMessageView: UIView, CollectionCellContent {
     }
 
     private func setup() {
+        lockerImageView.contentMode = .center
+        lockerImageView.image = Asset.chatLocker.image
+
         let attrString = NSAttributedString(
             string: "show mix",
             attributes: [
@@ -81,7 +84,7 @@ final class ReplyStackMessageView: UIView, CollectionCellContent {
 
         bottomStack.addArrangedSubview(roundButtonContainer)
         bottomStack.addArrangedSubview(dateLabel)
-        bottomStack.addArrangedSubview(lockerView)
+        bottomStack.addArrangedSubview(lockerImageView)
 
         bottomStack.setContentCompressionResistancePriority(.required, for: .horizontal)
 
diff --git a/Sources/ChatFeature/Views/Cells/StackMessageView.swift b/Sources/ChatFeature/Views/Cells/StackMessageView.swift
index 19e7361b..2c5a3c9f 100644
--- a/Sources/ChatFeature/Views/Cells/StackMessageView.swift
+++ b/Sources/ChatFeature/Views/Cells/StackMessageView.swift
@@ -6,15 +6,16 @@ typealias OutgoingTextCell = CollectionCell<FlexibleSpace, StackMessageView>
 typealias OutgoingFailedTextCell = CollectionCell<FlexibleSpace, StackMessageView>
 
 final class StackMessageView: UIView, CollectionCellContent {
-    let roundButton = UIButton()
-    let dateLabel = UILabel()
-    let textView = TextView()
-    let lockerView = LockerView()
-    let senderLabel = UILabel()
     private let stackView = UIStackView()
     private let shapeLayer = CAShapeLayer()
     private let bottomStack = UIStackView()
 
+    private(set) var dateLabel = UILabel()
+    private(set) var textView = TextView()
+    private(set) var senderLabel = UILabel()
+    private(set) var roundButton = UIButton()
+    private(set) var lockerImageView = UIImageView()
+
     var didTapShowRound: (() -> Void)?
 
     override init(frame: CGRect) {
@@ -34,7 +35,6 @@ final class StackMessageView: UIView, CollectionCellContent {
         textView.text = nil
         senderLabel.text = nil
         textView.resignFirstResponder()
-        lockerView.icon.layer.removeAllAnimations()
         didTapShowRound = nil
     }
 
@@ -44,6 +44,9 @@ final class StackMessageView: UIView, CollectionCellContent {
     }
 
     private func setup() {
+        lockerImageView.contentMode = .center
+        lockerImageView.image = Asset.chatLocker.image
+
         roundButton.addTarget(
             self,
             action: #selector(didTapRoundButton),
@@ -70,7 +73,8 @@ final class StackMessageView: UIView, CollectionCellContent {
         bottomStack.addArrangedSubview(FlexibleSpace())
         bottomStack.addArrangedSubview(roundButton)
         bottomStack.addArrangedSubview(dateLabel)
-        bottomStack.addArrangedSubview(lockerView)
+        bottomStack.addArrangedSubview(lockerImageView)
+        bottomStack.setCustomSpacing(8, after: dateLabel)
 
         stackView.spacing = 6
         stackView.axis = .vertical
diff --git a/Sources/ChatFeature/Views/LockerView.swift b/Sources/ChatFeature/Views/LockerView.swift
deleted file mode 100644
index 552fed89..00000000
--- a/Sources/ChatFeature/Views/LockerView.swift
+++ /dev/null
@@ -1,44 +0,0 @@
-import UIKit
-import Shared
-
-final class LockerView: UIView {
-    let icon = UIImageView()
-    let animation = CABasicAnimation()
-
-    init() {
-        super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    public func stop() {
-        icon.layer.removeAllAnimations()
-        icon.layer.opacity = 1.0
-    }
-
-    public func fail() {
-        icon.layer.removeAllAnimations()
-        icon.layer.opacity = 0.3
-    }
-
-    public func animate() {
-        icon.layer.removeAllAnimations()
-        icon.layer.add(animation, forKey: "opacity")
-    }
-
-    private func setup() {
-        animation.fromValue = 1.0
-        animation.toValue = 0.0
-        animation.duration = 0.5
-        animation.autoreverses = true
-        animation.repeatCount = .infinity
-
-        icon.contentMode = .center
-        icon.image = Asset.chatLocker.image
-
-        addSubview(icon)
-
-        icon.snp.makeConstraints { $0.edges.equalToSuperview().inset(5) }
-    }
-}
diff --git a/Sources/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift
index 20dce028..d2d15a40 100644
--- a/Sources/ChatListFeature/Controller/ChatListController.swift
+++ b/Sources/ChatListFeature/Controller/ChatListController.swift
@@ -1,260 +1,228 @@
-import HUD
-import DrawerFeature
 import UIKit
 import Theme
+import Models
 import Shared
 import Combine
 import MenuFeature
 import DependencyInjection
 
 public final class ChatListController: UIViewController {
-    @Dependency private var hud: HUDType
     @Dependency private var coordinator: ChatListCoordinating
     @Dependency private var statusBarController: StatusBarStyleControlling
 
-    lazy private var menu = UIButton()
-    lazy private var cancel = UIButton()
-    lazy private var menuBadge = UILabel()
-    lazy private var titleLabel = UILabel()
     lazy private var screenView = ChatListView()
-    lazy private var menuView = ChatListMenuView()
-    lazy private var contactListButton = UIButton()
-    lazy private var contactSearchButton = UIButton()
+    lazy private var topLeftView = ChatListTopLeftNavView()
+    lazy private var topRightView = ChatListTopRightNavView()
     lazy private var tableController = ChatListTableController(viewModel)
+    lazy private var searchTableController = ChatSearchTableController(viewModel)
+    private var collectionDataSource: UICollectionViewDiffableDataSource<SectionId, Contact>!
 
-    private var shouldPresentMenu = false
     private let viewModel = ChatListViewModel()
     private var cancellables = Set<AnyCancellable>()
     private var drawerCancellables = Set<AnyCancellable>()
 
-    public override var canBecomeFirstResponder: Bool { true }
-
-    public override var inputAccessoryView: UIView? {
-        if shouldPresentMenu {
-            tableController.numberOfSelectedRows = 0
+    private var isEditingSearch = false {
+        didSet {
+            screenView.listContainerView
+                .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents)
         }
-
-        return shouldPresentMenu ? menuView : nil
     }
 
-    public init() {
-        super.init(nibName: nil, bundle: nil)
+    private var shouldBeShowingRecents = false {
+        didSet {
+            screenView.listContainerView
+                .showRecentsCollection(isEditingSearch ? false : shouldBeShowingRecents)
+        }
     }
 
-    required init?(coder: NSCoder) { nil }
-
     public override func loadView() {
         view = screenView
-
-        addChild(tableController)
-        screenView.insertSubview(tableController.view, belowSubview: screenView)
-
-        tableController.view.snp.makeConstraints { make in
-            make.top.equalTo(screenView.searchView.snp.bottom)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview()
-        }
-
-        tableController.didMove(toParent: self)
     }
 
     public override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         statusBarController.style.send(.darkContent)
-        updateNavigationItems(false)
-
         navigationController?.navigationBar.customize(backgroundColor: Asset.neutralWhite.color)
     }
 
     public override func viewDidLoad() {
         super.viewDidLoad()
-        setupNavigationBar()
+        setupChatList()
         setupBindings()
+        setupNavigationBar()
+        setupRecentContacts()
     }
 
     private func setupNavigationBar() {
         navigationItem.backButtonTitle = ""
+        navigationItem.leftBarButtonItem = UIBarButtonItem(customView: topLeftView)
+        navigationItem.rightBarButtonItem = UIBarButtonItem(customView: topRightView)
+
+        topRightView.actionPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                switch $0 {
+                case .didTapSearch:
+                    coordinator.toSearch(from: self)
+                case .didTapNewGroup:
+                    coordinator.toNewGroup(from: self)
+                }
+            }.store(in: &cancellables)
 
-        contactSearchButton.tintColor = Asset.neutralDark.color
-        contactSearchButton.setImage(Asset.contactListSearch.image, for: .normal)
-        contactSearchButton.addTarget(self, action: #selector(didTapContactSearchButton), for: .touchUpInside)
-
-        contactListButton.tintColor = Asset.neutralDark.color
-        contactListButton.setImage(Asset.chatListNew.image, for: .normal)
-        contactListButton.addTarget(self, action: #selector(didTapContactListButton), for: .touchUpInside)
-
-        titleLabel.text = Localized.ChatList.title
-        titleLabel.textColor = Asset.neutralActive.color
-        titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
-
-        menu.tintColor = Asset.neutralDark.color
-        menu.setImage(Asset.chatListMenu.image, for: .normal)
-        menu.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside)
-        menu.snp.makeConstraints { $0.width.equalTo(50) }
-
-        menu.addSubview(menuBadge)
-        menuBadge.layer.cornerRadius = 5
-        menuBadge.layer.masksToBounds = true
-        menuBadge.snp.makeConstraints { make in
-            make.centerY.equalTo(menu.snp.top)
-            make.centerX.equalTo(menu.snp.right).multipliedBy(0.8)
+        viewModel.badgeCountPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in topLeftView.updateBadge($0) }
+            .store(in: &cancellables)
+
+        topLeftView.actionPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in coordinator.toSideMenu(from: self) }
+            .store(in: &cancellables)
+   }
+
+    private func setupChatList() {
+        addChild(tableController)
+        addChild(searchTableController)
+
+        screenView.listContainerView.addSubview(tableController.view)
+        screenView.searchListContainerView.addSubview(searchTableController.view)
+
+        tableController.view.snp.makeConstraints {
+            $0.top.equalTo(screenView.listContainerView.collectionContainerView.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
         }
 
-        menuBadge.textColor = Asset.neutralWhite.color
-        menuBadge.backgroundColor = Asset.brandPrimary.color
-        menuBadge.font = Fonts.Mulish.bold.font(size: 14.0)
+        searchTableController.view.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
 
-        cancel.setTitleColor(Asset.neutralActive.color, for: .normal)
-        cancel.titleLabel?.font = Fonts.Mulish.semiBold.font(size: 14.0)
-        cancel.setTitle(Localized.ChatList.NavigationBar.cancel, for: .normal)
+        tableController.didMove(toParent: self)
+        searchTableController.didMove(toParent: self)
+    }
 
-        menu.accessibilityIdentifier = Localized.Accessibility.ChatList.menu
+    private func setupRecentContacts() {
+        screenView
+            .listContainerView
+            .collectionView
+            .register(ChatListRecentContactCell.self)
+
+        collectionDataSource = UICollectionViewDiffableDataSource<SectionId, Contact>(
+            collectionView: screenView.listContainerView.collectionView
+        ) { collectionView, indexPath, contact in
+            let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
+            cell.setup(title: contact.nickname ?? contact.username, image: contact.photo)
+            return cell
+        }
+
+        screenView.listContainerView.collectionView.delegate = self
+        screenView.listContainerView.collectionView.dataSource = collectionDataSource
+
+        viewModel.recentsPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                collectionDataSource.apply($0)
+                shouldBeShowingRecents = $0.numberOfItems > 0
+            }.store(in: &cancellables)
     }
 
     private func setupBindings() {
-        viewModel.hud
+        screenView.searchView
+            .rightPublisher
             .receive(on: DispatchQueue.main)
-            .sink { [hud] in hud.update(with: $0) }
+            .sink { [unowned self] in coordinator.toScan(from: self) }
             .store(in: &cancellables)
 
-        viewModel.chatsRelay
+        screenView.searchView
+            .textPublisher
+            .removeDuplicates()
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in
-                screenView.stackView.isHidden = !$0.isEmpty
+            .sink { [unowned self] query in
+                viewModel.updateSearch(query: query)
+                screenView.searchListContainerView.emptyView.updateSearched(content: query)
+            }.store(in: &cancellables)
 
-                if $0.isEmpty {
-                    screenView.bringSubviewToFront(screenView.stackView)
+        Publishers.CombineLatest(
+            viewModel.searchPublisher,
+            screenView.searchView.textPublisher.removeDuplicates()
+        )
+        .receive(on: DispatchQueue.main)
+            .sink { [unowned self] items, query in
+                guard query.isEmpty == false else {
+                    screenView.searchListContainerView.isHidden = true
+                    screenView.listContainerView.isHidden = false
+                    screenView.bringSubviewToFront(screenView.listContainerView)
+                    return
                 }
-            }.store(in: &cancellables)
 
-        screenView.contactsButton
-            .publisher(for: .touchUpInside)
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toContacts(from: self) }
-            .store(in: &cancellables)
+                screenView.listContainerView.isHidden = true
+                screenView.searchListContainerView.isHidden = false
 
-        screenView.searchView.textPublisher
-            .removeDuplicates()
-            .sink { [unowned self] in viewModel.searchQueryRelay.send($0) }
+                guard items.numberOfItems > 0 else {
+                    screenView.searchListContainerView.emptyView.isHidden = false
+                    screenView.bringSubviewToFront(screenView.searchListContainerView)
+                    screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView)
+                    return
+                }
+
+                screenView.searchListContainerView.bringSubviewToFront(searchTableController.view)
+                screenView.searchListContainerView.emptyView.isHidden = true
+            }
             .store(in: &cancellables)
 
-        screenView.searchView.rightPublisher
+        screenView.searchView
+            .isEditingPublisher
+            .removeDuplicates()
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toScan(from: self) }
+            .sink { [unowned self] in isEditingSearch = $0 }
             .store(in: &cancellables)
 
-        tableController.deletePublisher
+        viewModel.chatsPublisher
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] ip in
-                if viewModel.isGroup(indexPath: ip) {
-                    presentDrawer(
-                        title: Localized.ChatList.DeleteGroup.title,
-                        subtitle: Localized.ChatList.DeleteGroup.subtitle,
-                        actionTitle: Localized.ChatList.DeleteGroup.action) { [weak self] in
-                            self?.viewModel.deleteAndLeaveGroupFrom(indexPath: ip)
-                        }
-                } else {
-                    presentDrawer(
-                        title: Localized.ChatList.Delete.title,
-                        subtitle: Localized.ChatList.Delete.subtitle,
-                        actionTitle: Localized.ChatList.Delete.delete) { [weak self] in
-                            self?.viewModel.delete(indexPaths: [ip])
-                        }
+            .sink { [unowned self] in
+                guard $0.isEmpty == false else {
+                    screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView)
+                    screenView.listContainerView.emptyView.isHidden = false
+                    return
                 }
-            }.store(in: &cancellables)
 
-        viewModel.badgeCount
+                screenView.listContainerView.bringSubviewToFront(tableController.view)
+                screenView.listContainerView.emptyView.isHidden = true
+            }
+            .store(in: &cancellables)
+
+        screenView.searchListContainerView
+            .emptyView.searchButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in coordinator.toSearch(from: self) }
+            .store(in: &cancellables)
+
+        screenView.listContainerView
+            .emptyView.contactsButton
+            .publisher(for: .touchUpInside)
             .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in
-                menuBadge.text = " \($0) "
-                menuBadge.isHidden = $0 < 1
-            }.store(in: &cancellables)
+            .sink { [unowned self] in coordinator.toContacts(from: self) }
+            .store(in: &cancellables)
 
         viewModel.isOnline
             .removeDuplicates()
             .receive(on: DispatchQueue.main)
-            .sink { [weak screenView] in screenView?.displayNetworkIssue(!$0) }
+            .sink { [weak screenView] connected in screenView?.showConnectingBanner(!connected) }
             .store(in: &cancellables)
     }
+}
 
-    private func updateNavigationItems(_ isEditing: Bool) {
-        let leftStack = UIStackView()
-        leftStack.addArrangedSubview(titleLabel)
-
-        let rightStack = UIStackView()
-        rightStack.spacing = 10
-        rightStack.addArrangedSubview(contactListButton)
-        rightStack.addArrangedSubview(contactSearchButton)
-
-        contactListButton.snp.makeConstraints { $0.width.equalTo(40) }
-        contactSearchButton.snp.makeConstraints { $0.width.equalTo(40) }
-
-        if !isEditing {
-            leftStack.insertArrangedSubview(menu, at: 0)
-            navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftStack)
-        } else {
-            navigationItem.leftBarButtonItem = UIBarButtonItem(
-                customView: leftStack.pinning(at: .left(10))
-            )
-        }
-
-        navigationItem.rightBarButtonItem = UIBarButtonItem(
-            customView: isEditing ? cancel : rightStack
-        )
-    }
-
-    @objc private func didTapContactListButton() {
-        coordinator.toContacts(from: self)
-    }
-
-    @objc private func didTapContactSearchButton() {
-        coordinator.toSearch(from: self)
-    }
-
-    @objc private func didTapMenu() {
-        coordinator.toSideMenu(from: self)
-    }
-
-    private func presentDrawer(
-        title: String,
-        subtitle: String,
-        actionTitle: String,
-        action: @escaping () -> Void
+extension ChatListController: UICollectionViewDelegate {
+    public func collectionView(
+        _ collectionView: UICollectionView,
+        didSelectItemAt indexPath: IndexPath
     ) {
-        let actionButton = DrawerCapsuleButton(model: .init(
-            title: actionTitle,
-            style: .red
-        ))
-
-        let drawer = DrawerController(with: [
-            DrawerText(
-                font: Fonts.Mulish.bold.font(size: 26.0),
-                text: title,
-                color: Asset.neutralActive.color,
-                alignment: .left,
-                spacingAfter: 19
-            ),
-            DrawerText(
-                font: Fonts.Mulish.regular.font(size: 16.0),
-                text: subtitle,
-                color: Asset.neutralBody.color,
-                alignment: .left,
-                lineHeightMultiple: 1.1,
-                spacingAfter: 39
-            ),
-            actionButton
-        ])
-
-        actionButton.action.receive(on: DispatchQueue.main)
-            .sink {
-                drawer.dismiss(animated: true) { [weak self] in
-                    guard let self = self else { return }
-                    self.drawerCancellables.removeAll()
-                    action()
-                }
-            }.store(in: &drawerCancellables)
-
-        coordinator.toDrawer(drawer, from: self)
+        if let contact = collectionDataSource.itemIdentifier(for: indexPath) {
+            coordinator.toSingleChat(with: contact, from: self)
+        }
     }
 }
diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift
new file mode 100644
index 00000000..c1abc94a
--- /dev/null
+++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift
@@ -0,0 +1,128 @@
+import UIKit
+import Shared
+import Models
+import Combine
+import DependencyInjection
+
+class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> {
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        switch snapshot().sectionIdentifiers[section] {
+        case .chats:
+            return "CHATS"
+        case .connections:
+            return "CONNECTIONS"
+        }
+    }
+}
+
+final class ChatSearchTableController: UITableViewController {
+    @Dependency private var coordinator: ChatListCoordinating
+
+    private let viewModel: ChatListViewModel
+    private let cellHeight: CGFloat = 83.0
+    private var cancellables = Set<AnyCancellable>()
+    private var tableDataSource: ChatSearchListTableViewDiffableDataSource?
+
+    init(_ viewModel: ChatListViewModel) {
+        self.viewModel = viewModel
+        super.init(style: .grouped)
+
+        tableDataSource = ChatSearchListTableViewDiffableDataSource(
+            tableView: tableView
+        ) { table, indexPath, item in
+            let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
+            switch item {
+            case .chat(let subitem):
+                if case .contact(let info) = subitem {
+                    cell.setupContact(
+                        name: info.contact.nickname ?? info.contact.username,
+                        image: info.contact.photo,
+                        date: Date.fromTimestamp(info.lastMessage!.timestamp),
+                        hasUnread: info.lastMessage!.unread,
+                        preview: info.lastMessage!.payload.text
+                    )
+                }
+
+                if case .group(let info) = subitem {
+                    let date: Date = {
+                        guard let lastMessage = info.lastMessage else {
+                            return info.group.createdAt
+                        }
+
+                        return Date.fromTimestamp(lastMessage.timestamp)
+                    }()
+
+                    let hasUnread: Bool = {
+                        guard let lastMessage = info.lastMessage else {
+                            return false
+                        }
+
+                        return lastMessage.unread
+                    }()
+
+                    cell.setupGroup(
+                        name: info.group.name,
+                        date: date,
+                        preview: info.lastMessage?.payload.text,
+                        hasUnread: hasUnread
+                    )
+                }
+
+            case .connection(let contact):
+                cell.setupContact(
+                    name: contact.nickname ?? contact.username,
+                    image: contact.photo,
+                    date: nil,
+                    hasUnread: false,
+                    preview: contact.username
+                )
+            }
+
+            return cell
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        tableView.separatorStyle = .none
+        tableView.tableFooterView = UIView()
+        tableView.sectionIndexColor = .blue
+        tableView.register(ChatListCell.self)
+        tableView.dataSource = tableDataSource
+        view.backgroundColor = Asset.neutralWhite.color
+
+        viewModel.searchPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) }
+            .store(in: &cancellables)
+    }
+}
+
+extension ChatSearchTableController {
+    override func tableView(
+        _ tableView: UITableView,
+        heightForRowAt: IndexPath
+    ) -> CGFloat {
+        return cellHeight
+    }
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        if let item = tableDataSource?.itemIdentifier(for: indexPath) {
+            switch item {
+            case .chat(let chat):
+                switch chat {
+                case .contact(let info):
+                    guard info.contact.status == .friend else { return }
+                    coordinator.toSingleChat(with: info.contact, from: self)
+                case .group(let info):
+                    coordinator.toGroupChat(with: info, from: self)
+                }
+            case .connection(let contact):
+                coordinator.toContact(contact, from: self)
+            }
+        }
+    }
+}
diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift
index af7f3c55..17b74411 100644
--- a/Sources/ChatListFeature/Controller/ChatListTableController.swift
+++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift
@@ -1,38 +1,21 @@
 import UIKit
 import Shared
-import Combine
 import Models
+import Combine
 import DifferenceKit
+import DrawerFeature
 import DependencyInjection
 
 final class ChatListTableController: UITableViewController {
-    // MARK: Injected
-
     @Dependency private var coordinator: ChatListCoordinating
 
-    // MARK: Published
-
-    @Published var numberOfSelectedRows = 0
-
-    // MARK: Properties
-
-    var longPressPublisher: AnyPublisher<Void, Never> {
-        longPressRelay.eraseToAnyPublisher()
-    }
-
-    var deletePublisher: AnyPublisher<IndexPath, Never> {
-        deleteRelay.eraseToAnyPublisher()
-    }
-
-    private var rows = [GenericChatInfo]()
-    private let viewModel: ChatListViewModelType
+    private var rows = [Chat]()
+    private let viewModel: ChatListViewModel
+    private let cellHeight: CGFloat = 83.0
     private var cancellables = Set<AnyCancellable>()
-    private let longPressRelay = PassthroughSubject<Void, Never>()
-    private let deleteRelay = PassthroughSubject<IndexPath, Never>()
-
-    // MARK: Lifecycle
+    private var drawerCancellables = Set<AnyCancellable>()
 
-    init(_ viewModel: ChatListViewModelType) {
+    init(_ viewModel: ChatListViewModel) {
         self.viewModel = viewModel
         super.init(style: .grouped)
     }
@@ -41,25 +24,15 @@ final class ChatListTableController: UITableViewController {
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        setupTableView()
-        setupBindings()
-    }
 
-    // MARK: Private
-
-    private func setupTableView() {
         tableView.separatorStyle = .none
         tableView.backgroundColor = .clear
         tableView.alwaysBounceVertical = true
         tableView.register(ChatListCell.self)
-        tableView.tintColor = Asset.brandPrimary.color
-        tableView.allowsMultipleSelectionDuringEditing = true
         tableView.tableFooterView = UIView()
-    }
 
-    private func setupBindings() {
         viewModel
-            .chatsRelay
+            .chatsPublisher
             .receive(on: DispatchQueue.main)
             .sink { [unowned self] in
                 guard !self.rows.isEmpty else {
@@ -81,79 +54,144 @@ final class ChatListTableController: UITableViewController {
                 }
             }.store(in: &cancellables)
     }
+}
 
-    // MARK: UITableViewDataSource
-
-    override func tableView(_ tableView: UITableView,
-                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
-        let chatInfo = rows[indexPath.row]
-
-        if let contact = chatInfo.contact {
-            let name = contact.nickname ?? contact.username
-            cell.titleLabel.text = name
-            cell.avatarView.setupProfile(title: name, image: chatInfo.contact?.photo, size: .large)
-        } else {
-            cell.titleLabel.text = chatInfo.groupInfo!.group.name
-            cell.avatarView.setupGroup(size: .large)
-        }
-
-        cell.didLongPress = { [weak longPressRelay] in
-            longPressRelay?.send()
-        }
-
-        if let latestGroupMessage = chatInfo.groupInfo?.lastMessage {
-            cell.titleLabel.alpha = 1.0
-            cell.avatarView.alpha = 1.0
-            cell.date = Date.fromTimestamp(latestGroupMessage.timestamp)
-            cell.previewLabel.text = latestGroupMessage.payload.text
-            cell.unreadView.backgroundColor = latestGroupMessage.unread ? Asset.brandPrimary.color : .clear
-        }
-
-        if let latestE2EMessage = chatInfo.latestE2EMessage {
-            cell.titleLabel.alpha = 1.0
-            cell.avatarView.alpha = 1.0
-            cell.date = Date.fromTimestamp(latestE2EMessage.timestamp)
-            cell.previewLabel.text = latestE2EMessage.payload.text
-            cell.unreadView.backgroundColor = latestE2EMessage.unread ? Asset.brandPrimary.color : .clear
-        }
-
-        return cell
+extension ChatListTableController {
+    override func tableView(
+        _ tableView: UITableView,
+        numberOfRowsInSection: Int
+    ) -> Int {
+        return rows.count
     }
 
-    override func tableView(_: UITableView, numberOfRowsInSection: Int) -> Int { rows.count }
+    override func tableView(
+        _ tableView: UITableView,
+        heightForRowAt: IndexPath
+    ) -> CGFloat {
+        return cellHeight
+    }
 
-    override func tableView(_: UITableView, heightForRowAt: IndexPath) -> CGFloat { 72 }
+    override func tableView(
+        _ tableView: UITableView,
+        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+    ) -> UISwipeActionsConfiguration? {
 
-    override func tableView(_: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
         let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in
-            self?.deleteRelay.send(indexPath)
+            guard let self = self else { return }
+            self.didRequestDeletionOf(self.rows[indexPath.row])
             complete(true)
         }
 
         delete.image = Asset.chatListDeleteSwipe.image
         delete.backgroundColor = Asset.accentDanger.color
-
         return UISwipeActionsConfiguration(actions: [delete])
     }
 
     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        if !tableView.isEditing {
-            let genericChat = viewModel.chatsRelay.value[indexPath.row]
-
-            guard let contact = genericChat.contact else {
-                coordinator.toGroupChat(with: genericChat.groupInfo!, from: self)
-                return
-            }
-
-            guard contact.status == .friend else { return }
-            coordinator.toSingleChat(with: contact, from: self)
-        } else {
-            numberOfSelectedRows += 1
+        switch rows[indexPath.row] {
+        case .contact(let info):
+            guard info.contact.status == .friend else { return }
+            coordinator.toSingleChat(with: info.contact, from: self)
+        case .group(let info):
+            coordinator.toGroupChat(with: info, from: self)
+        }
+    }
+
+    override func tableView(
+        _ tableView: UITableView,
+        cellForRowAt indexPath: IndexPath
+    ) -> UITableViewCell {
+
+        let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
+
+        if case .contact(let info) = rows[indexPath.row] {
+            cell.setupContact(
+                name: info.contact.nickname ?? info.contact.username,
+                image: info.contact.photo,
+                date: Date.fromTimestamp(info.lastMessage!.timestamp),
+                hasUnread: info.lastMessage!.unread,
+                preview: info.lastMessage!.payload.text
+            )
+        }
+
+        if case .group(let info) = rows[indexPath.row] {
+            let date: Date = {
+                guard let lastMessage = info.lastMessage else {
+                    return info.group.createdAt
+                }
+
+                return Date.fromTimestamp(lastMessage.timestamp)
+            }()
+
+            let hasUnread: Bool = {
+                guard let lastMessage = info.lastMessage else {
+                    return false
+                }
+
+                return lastMessage.unread
+            }()
+
+            cell.setupGroup(
+                name: info.group.name,
+                date: date,
+                preview: info.lastMessage?.payload.text,
+                hasUnread: hasUnread
+            )
         }
+
+        return cell
     }
 
-    override func tableView(_: UITableView, didDeselectRowAt: IndexPath) {
-        numberOfSelectedRows -= 1
+    private func didRequestDeletionOf(_ item: Chat) {
+        let title: String
+        let subtitle: String
+        let actionTitle: String
+        let actionClosure: () -> Void
+
+        switch item {
+        case .group(let info):
+            title = Localized.ChatList.DeleteGroup.title
+            subtitle = Localized.ChatList.DeleteGroup.subtitle
+            actionTitle = Localized.ChatList.DeleteGroup.action
+            actionClosure = { [weak viewModel] in viewModel?.leave(info.group) }
+
+        case .contact(let info):
+            title = Localized.ChatList.Delete.title
+            subtitle = Localized.ChatList.Delete.subtitle
+            actionTitle = Localized.ChatList.Delete.delete
+            actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) }
+        }
+
+        let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red))
+
+        let drawer = DrawerController(with: [
+            DrawerText(
+                font: Fonts.Mulish.bold.font(size: 26.0),
+                text: title,
+                color: Asset.neutralActive.color,
+                alignment: .left,
+                spacingAfter: 19
+            ),
+            DrawerText(
+                font: Fonts.Mulish.regular.font(size: 16.0),
+                text: subtitle,
+                color: Asset.neutralBody.color,
+                alignment: .left,
+                lineHeightMultiple: 1.1,
+                spacingAfter: 39
+            ),
+            actionButton
+        ])
+
+        actionButton.action.receive(on: DispatchQueue.main)
+            .sink {
+                drawer.dismiss(animated: true) { [weak self] in
+                    guard let self = self else { return }
+                    self.drawerCancellables.removeAll()
+                    actionClosure()
+                }
+            }.store(in: &drawerCancellables)
+
+        coordinator.toDrawer(drawer, from: self)
     }
 }
diff --git a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift
index 75958ab2..42b7dda6 100644
--- a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift
+++ b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift
@@ -11,7 +11,9 @@ public protocol ChatListCoordinating {
     func toScan(from: UIViewController)
     func toSearch(from: UIViewController)
     func toContacts(from: UIViewController)
+    func toNewGroup(from: UIViewController)
     func toSideMenu(from: UIViewController)
+    func toContact(_: Contact, from: UIViewController)
     func toSingleChat(with: Contact, from: UIViewController)
     func toDrawer(_: UIViewController, from: UIViewController)
     func toGroupChat(with: GroupChatInfo, from: UIViewController)
@@ -25,7 +27,9 @@ public struct ChatListCoordinator: ChatListCoordinating {
 
     var scanFactory: () -> UIViewController
     var searchFactory: () -> UIViewController
+    var newGroupFactory: () -> UIViewController
     var contactsFactory: () -> UIViewController
+    var contactFactory: (Contact) -> UIViewController
     var singleChatFactory: (Contact) -> UIViewController
     var groupChatFactory: (GroupChatInfo) -> UIViewController
     var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController
@@ -33,13 +37,17 @@ public struct ChatListCoordinator: ChatListCoordinating {
     public init(
         scanFactory: @escaping () -> UIViewController,
         searchFactory: @escaping () -> UIViewController,
+        newGroupFactory: @escaping () -> UIViewController,
         contactsFactory: @escaping () -> UIViewController,
+        contactFactory: @escaping (Contact) -> UIViewController,
         singleChatFactory: @escaping (Contact) -> UIViewController,
         groupChatFactory: @escaping (GroupChatInfo) -> UIViewController,
         sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController
     ) {
         self.scanFactory = scanFactory
         self.searchFactory = searchFactory
+        self.contactFactory = contactFactory
+        self.newGroupFactory = newGroupFactory
         self.contactsFactory = contactsFactory
         self.sideMenuFactory = sideMenuFactory
         self.groupChatFactory = groupChatFactory
@@ -63,6 +71,11 @@ public extension ChatListCoordinator {
         pushPresenter.present(screen, from: parent)
     }
 
+    func toContact(_ contact: Contact, from parent: UIViewController) {
+        let screen = contactFactory(contact)
+        pushPresenter.present(screen, from: parent)
+    }
+
     func toSingleChat(with contact: Contact, from parent: UIViewController) {
         let screen = singleChatFactory(contact)
         pushPresenter.present(screen, from: parent)
@@ -78,6 +91,11 @@ public extension ChatListCoordinator {
         sidePresenter.present(screen, from: parent)
     }
 
+    func toNewGroup(from parent: UIViewController) {
+        let screen = newGroupFactory()
+        pushPresenter.present(screen, from: parent)
+    }
+
     func toDrawer(_ drawer: UIViewController, from parent: UIViewController) {
         bottomPresenter.present(drawer, from: parent)
     }
diff --git a/Sources/ChatListFeature/Models/Chat.swift b/Sources/ChatListFeature/Models/Chat.swift
new file mode 100644
index 00000000..3e159479
--- /dev/null
+++ b/Sources/ChatListFeature/Models/Chat.swift
@@ -0,0 +1,34 @@
+import Models
+import Foundation
+import DifferenceKit
+
+enum Chat: Equatable, Differentiable, Hashable {
+    case group(GroupChatInfo)
+    case contact(SingleChatInfo)
+
+    var differenceIdentifier: Data {
+        switch self {
+        case .contact(let info):
+            return info.contact.userId
+        case .group(let info):
+            return info.group.groupId
+        }
+    }
+
+    var orderingDate: Date {
+        switch self {
+        case .group(let info):
+            if let lastMessage = info.lastMessage {
+                return Date.fromTimestamp(lastMessage.timestamp)
+            } else {
+                return info.group.createdAt
+            }
+        case .contact(let info):
+            guard let lastMessage = info.lastMessage else {
+                fatalError("Should have an E2E chat without a last message")
+            }
+
+            return Date.fromTimestamp(lastMessage.timestamp)
+        }
+    }
+}
diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
index a78407c0..ceb096f3 100644
--- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
+++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift
@@ -1,190 +1,141 @@
 import HUD
+import UIKit
 import Shared
 import Models
 import Combine
 import Defaults
-import Foundation
 import Integration
 import DependencyInjection
 
-protocol ChatListViewModelType {
-    var myId: Data { get }
-    var username: String { get }
-    var editState: EditStateHandler { get }
-    var searchQueryRelay: CurrentValueSubject<String, Never> { get }
-    var chatsRelay: CurrentValueSubject<[GenericChatInfo], Never> { get }
-
-    var isOnline: AnyPublisher<Bool, Never> { get }
-    var badgeCount: AnyPublisher<Int, Never> { get }
+enum SearchSection {
+    case chats
+    case connections
+}
 
-    func delete(indexPaths: [IndexPath]?)
+enum SearchItem: Equatable, Hashable {
+    case chat(Chat)
+    case connection(Contact)
 }
 
-final class ChatListViewModel: ChatListViewModelType {
+typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>
+typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
+
+final class ChatListViewModel {
     @Dependency private var session: SessionType
 
-    @KeyObject(.username, defaultValue: "") var myUsername: String
+    var isOnline: AnyPublisher<Bool, Never> {
+        session.isOnline
+    }
 
-    let editState = EditStateHandler()
-    let chatsRelay = CurrentValueSubject<[GenericChatInfo], Never>([])
-    let searchQueryRelay = CurrentValueSubject<String, Never>("")
-    private var cancellables = Set<AnyCancellable>()
+    var chatsPublisher: AnyPublisher<[Chat], Never> {
+        chatsSubject.eraseToAnyPublisher()
+    }
 
-    var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
-    private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
+
+    var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> {
+        session.contacts(.isRecent).map {
+            let section = SectionId()
+            var snapshot = RecentsSnapshot()
+            snapshot.appendSections([section])
+            snapshot.appendItems($0, toSection: section)
+            return snapshot
+        }.eraseToAnyPublisher()
+    }
+
+    var searchPublisher: AnyPublisher<SearchSnapshot, Never> {
+        Publishers.CombineLatest3(
+            session.contacts(.all),
+            chatsPublisher,
+            searchSubject
+                .removeDuplicates()
+                .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
+                .eraseToAnyPublisher()
+        )
+            .map { (contacts, chats, query) in
+                let connectionItems = contacts.filter {
+                    let username = $0.username.lowercased().contains(query.lowercased())
+                    let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false
+                    return username || nickname
+                }.map(SearchItem.connection)
+
+                let chatItems = chats.filter {
+                    switch $0 {
+                    case .contact(let info):
+                        let username = info.contact.username.lowercased().contains(query.lowercased())
+                        let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false
+                        let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
+                        return username || nickname || lastMessage
+
+                    case .group(let info):
+                        let name = info.group.name.lowercased().contains(query.lowercased())
+                        let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
+                        return name || last
+                    }
+                }.map(SearchItem.chat)
+
+                var snapshot = SearchSnapshot()
+
+                if connectionItems.count > 0 {
+                    snapshot.appendSections([.connections])
+                    snapshot.appendItems(connectionItems, toSection: .connections)
+                }
+
+                if chatItems.count > 0 {
+                    snapshot.appendSections([.chats])
+                    snapshot.appendItems(chatItems, toSection: .chats)
+                }
+
+                return snapshot
+            }.eraseToAnyPublisher()
+    }
 
-    var badgeCount: AnyPublisher<Int, Never> {
+    var badgeCountPublisher: AnyPublisher<Int, Never> {
         Publishers.CombineLatest(
             session.contacts(.received),
             session.groups(.pending)
-        ).map { $0.0.count + $0.1.count }
+        )
+        .map { $0.0.count + $0.1.count }
         .eraseToAnyPublisher()
     }
 
-    var isOnline: AnyPublisher<Bool, Never> { session.isOnline }
-
-    var myId: Data { session.myId }
-
-    var username: String { myUsername }
+    private var cancellables = Set<AnyCancellable>()
+    private let searchSubject = CurrentValueSubject<String, Never>("")
+    private let chatsSubject = CurrentValueSubject<[Chat], Never>([])
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
 
     init() {
-        let searchStream = searchQueryRelay
-            .removeDuplicates()
-            .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
-            .eraseToAnyPublisher()
-
-        Publishers.CombineLatest3(
+        Publishers.CombineLatest(
             session.singleChats(.all),
-            session.groupChats(.accepted),
-            searchStream
-        ).map { data -> [GenericChatInfo] in
-            let singles = data.0
-            let groupies = data.1
-            let searched = data.2
-
-            var generics = [GenericChatInfo]()
-
-            for single in singles {
-                generics.append(
-                    GenericChatInfo(
-                        contact: single.contact,
-                        groupInfo: nil,
-                        latestE2EMessage: single.lastMessage
-                    )
-                )
-            }
-
-            for group in groupies {
-                generics.append(
-                    GenericChatInfo(
-                        contact: nil,
-                        groupInfo: group,
-                        latestE2EMessage: nil
-                    )
-                )
-            }
-
-            if !searched.isEmpty {
-                generics = generics.filter { filtering in
-                    if let contact = filtering.contact {
-                        let username = contact.username.lowercased().contains(searched.lowercased())
-                        let nickname = contact.nickname?.lowercased().contains(searched.lowercased()) ?? false
-                        let lastMessage = filtering.latestE2EMessage?.payload.text.lowercased().contains(searched.lowercased()) ?? false
-
-                        return username || nickname || lastMessage
-                    } else {
-                        if let group = filtering.groupInfo?.group {
-                            let name = group.name.lowercased().contains(searched.lowercased())
-                            let last = filtering.groupInfo?.lastMessage?.payload.text.lowercased().contains(searched.lowercased()) ?? false
-                            return name || last
-                        }
-                    }
-
-                    return false
-                }
-            }
-
-            #warning("TODO: Use enum to differentiate chats")
-
-            return generics.sorted { infoA, infoB in
-                if let singleA = infoA.latestE2EMessage {
-                    if let singleB = infoB.latestE2EMessage {
-                        /// aSingle bSingle
-                        return singleA.timestamp > singleB.timestamp
-                    } else {
-                        /// aSingle bGroup
-                        let groupB = infoB.groupInfo!
-
-                        if let lastGM = groupB.lastMessage {
-                            /// aSingle bGroup w/ message
-                            return singleA.timestamp > lastGM.timestamp
-                        } else {
-                            /// aSingle bGroup w/out message
-                            return true
-                        }
-                    }
-                } else {
-                    let groupA = infoA.groupInfo!
-
-                    if let lastGM = groupA.lastMessage {
-                        /// aGroup w/ message
-
-                        if let singleB = infoB.latestE2EMessage {
-                            /// aGroup w/ message bSingle
-
-                            return lastGM.timestamp > singleB.timestamp
-                        } else {
-                            let groupB = infoB.groupInfo!
-                            /// aGroup w/ message bGroup
-
-                            if let lastGM2 = groupB.lastMessage {
-                                return lastGM.timestamp > lastGM2.timestamp
-                            } else {
-                                return true
-                            }
-                        }
-                    } else {
-                        /// aGroup w/out message b?
-                        return false
-                    }
-                }
-            }
-        }.sink { [unowned self] in chatsRelay.send($0)  }
+            session.groupChats(.accepted)
+        ).map {
+            let groups = $0.1.map(Chat.group)
+            let chats = $0.0.map(Chat.contact)
+            return (chats + groups).sorted { $0.orderingDate > $1.orderingDate }
+        }
+        .sink { [unowned self] in chatsSubject.send($0) }
         .store(in: &cancellables)
     }
 
-    func isGroup(indexPath: IndexPath) -> Bool {
-        chatsRelay.value[indexPath.row].contact == nil
+    func updateSearch(query: String) {
+        searchSubject.send(query)
     }
 
-    func deleteAndLeaveGroupFrom(indexPath: IndexPath) {
-        guard let group = chatsRelay.value[indexPath.row].groupInfo?.group else {
-            fatalError("Tried to delete a group from an index path that is not one")
-        }
+    func leave(_ group: Group) {
+        hudSubject.send(.on(nil))
 
         do {
-            hudRelay.send(.on(nil))
             try session.leave(group: group)
-            hudRelay.send(.none)
+            session.deleteAll(from: group)
+            hudSubject.send(.none)
         } catch {
-            hudRelay.send(.error(.init(with: error)))
+            hudSubject.send(.error(.init(with: error)))
         }
     }
 
-    func delete(indexPaths: [IndexPath]?) {
-        guard let selectedIndexPaths = indexPaths else {
-            let contacts = chatsRelay.value.compactMap { $0.contact }
-            let groups = chatsRelay.value.compactMap { $0.groupInfo?.group }
-
-            groups.forEach(session.deleteAll(from:))
-            contacts.forEach(session.deleteAll(from:))
-            return
-        }
-
-        let contacts = selectedIndexPaths.compactMap { chatsRelay.value[$0.row].contact }
-        let groups = selectedIndexPaths.compactMap { chatsRelay.value[$0.row].groupInfo?.group }
-
-        groups.forEach(session.deleteAll(from:))
-        contacts.forEach(session.deleteAll(from:))
+    func clear(_ contact: Contact) {
+        session.deleteAll(from: contact)
     }
 }
diff --git a/Sources/ChatListFeature/Views/ChatListCell.swift b/Sources/ChatListFeature/Views/ChatListCell.swift
index fe59df03..8eea62d2 100644
--- a/Sources/ChatListFeature/Views/ChatListCell.swift
+++ b/Sources/ChatListFeature/Views/ChatListCell.swift
@@ -2,99 +2,74 @@ import UIKit
 import Shared
 
 final class ChatListCell: UITableViewCell {
-    let titleLabel = UILabel()
-    let unreadView = UIView()
-    let previewLabel = UILabel()
-    let dateLabel = UILabel()
-    let avatarView = AvatarView()
-    let coloringView = UIView()
-
-    private var timer: Timer?
-
-    var date: Date? {
-        didSet {
-            updateTimeAgoLabel()
-        }
+    private let titleLabel = UILabel()
+    private let unreadView = UIView()
+    private let previewLabel = UILabel()
+    private let dateLabel = UILabel()
+    private let avatarView = AvatarView()
+    private var lastDate: Date? {
+        didSet { updateTimeAgoLabel() }
     }
 
-    deinit {
-        timer?.invalidate()
-    }
+    private var timer: Timer?
 
-    var didLongPress: EmptyClosure?
-    private let longPressGesture = UILongPressGestureRecognizer(target: nil, action: nil)
+    deinit { timer?.invalidate() }
 
     override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
         super.init(style: style, reuseIdentifier: reuseIdentifier)
 
-        longPressGesture.addTarget(self, action: #selector(longAction))
-        addGestureRecognizer(longPressGesture)
-
-        backgroundColor = .clear
-        selectedBackgroundView = UIView()
-        multipleSelectionBackgroundView = UIView()
-
-        timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in
-            self?.updateTimeAgoLabel()
-        }
-
         previewLabel.numberOfLines = 2
+        dateLabel.textAlignment = .right
+
         unreadView.layer.cornerRadius = 8
         avatarView.layer.cornerRadius = 21
-        dateLabel.textAlignment = .right
         avatarView.layer.masksToBounds = true
 
-        dateLabel.setContentHuggingPriority(.init(rawValue: 251), for: .vertical)
-        dateLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal)
-        dateLabel.setContentCompressionResistancePriority(.init(rawValue: 751), for: .vertical)
-        dateLabel.setContentCompressionResistancePriority(.init(rawValue: 751), for: .horizontal)
-
+        dateLabel.textAlignment = .right
+        selectedBackgroundView = UIView()
         unreadView.backgroundColor = .clear
         backgroundColor = Asset.neutralWhite.color
-        titleLabel.textColor = Asset.neutralActive.color
-        previewLabel.textColor = Asset.neutralDisabled.color
         dateLabel.textColor = Asset.neutralWeak.color
+        titleLabel.textColor = Asset.neutralActive.color
+
+        dateLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 16.0)
+
+
+        timer = Timer.scheduledTimer(withTimeInterval: 59, repeats: true) { [weak self] _ in
+            self?.updateTimeAgoLabel()
+        }
 
-        titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
-        previewLabel.font = Fonts.Mulish.regular.font(size: 12.0)
-        dateLabel.font = Fonts.Mulish.regular.font(size: 10.0)
+        dateLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
 
-        insertSubview(coloringView, belowSubview: contentView)
         contentView.addSubview(titleLabel)
         contentView.addSubview(unreadView)
         contentView.addSubview(avatarView)
         contentView.addSubview(previewLabel)
         contentView.addSubview(dateLabel)
 
-        coloringView.snp.makeConstraints {
-            $0.top.equalToSuperview().offset(6)
-            $0.left.equalToSuperview()
-            $0.right.equalToSuperview()
-            $0.bottom.equalToSuperview().offset(-6)
-        }
-
         avatarView.snp.makeConstraints {
-            $0.top.equalTo(coloringView).offset(6)
-            $0.left.equalToSuperview().offset(28)
+            $0.top.equalToSuperview().offset(14)
+            $0.left.equalToSuperview().offset(24)
             $0.width.height.equalTo(48)
-            $0.bottom.equalTo(coloringView).offset(-6)
         }
 
         titleLabel.snp.makeConstraints {
-            $0.top.equalTo(coloringView).offset(4)
+            $0.top.equalToSuperview().offset(10)
             $0.left.equalTo(avatarView.snp.right).offset(16)
             $0.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-10)
         }
 
         dateLabel.snp.makeConstraints {
             $0.top.equalTo(titleLabel)
-            $0.right.equalToSuperview().offset(-24)
+            $0.right.equalToSuperview().offset(-25)
         }
 
         previewLabel.snp.makeConstraints {
             $0.left.equalTo(titleLabel)
-            $0.top.equalTo(titleLabel.snp.bottom).offset(3)
+            $0.top.equalTo(titleLabel.snp.bottom).offset(2)
             $0.right.lessThanOrEqualTo(unreadView.snp.left).offset(-3)
+            $0.bottom.lessThanOrEqualToSuperview().offset(-10)
         }
 
         unreadView.snp.makeConstraints {
@@ -108,43 +83,65 @@ final class ChatListCell: UITableViewCell {
 
     override func prepareForReuse() {
         super.prepareForReuse()
-        date = nil
+        lastDate = nil
         titleLabel.text = nil
-        previewLabel.text = nil
+        previewLabel.attributedText = nil
         avatarView.prepareForReuse()
     }
 
-    override func willTransition(to state: UITableViewCell.StateMask) {
-        super.willTransition(to: state)
-
-        UIView.transition(with: coloringView, duration: 0.4, options: .transitionCrossDissolve) {
-            let isEditing = state == .showingEditControl
-            self.coloringView.backgroundColor = isEditing ?
-                Asset.neutralSecondary.color : Asset.neutralWhite.color
-        }
-
-        UIView.transition(with: dateLabel, duration: 0.4, options: .transitionCrossDissolve) {
-            let isEditing = state == .showingEditControl
-            self.dateLabel.alpha = isEditing ? 0.0 : 1.0
+    private func updateTimeAgoLabel() {
+        if let date = lastDate {
+            dateLabel.text = date.asRelativeFromNow()
         }
+    }
 
-        UIView.transition(with: avatarView, duration: 0.1, options: .transitionCrossDissolve) {
-            let isEditing = state == .showingEditControl
-
-            self.avatarView.snp.updateConstraints {
-                $0.left.equalToSuperview().offset(isEditing ? 16 : 28)
-            }
+    func setupContact(
+        name: String,
+        image: Data?,
+        date: Date?,
+        hasUnread: Bool,
+        preview: String
+    ) {
+        titleLabel.text = name
+        setPreview(string: preview)
+        avatarView.setupProfile(title: name, image: image, size: .large)
+        unreadView.backgroundColor = hasUnread ? Asset.brandPrimary.color : .clear
+
+        if let date = date {
+            lastDate = date
+        } else {
+            dateLabel.text = nil
         }
     }
 
-    private func updateTimeAgoLabel() {
-        guard let date = date else { return }
-        dateLabel.text = date.asRelativeFromNow()
+    func setupGroup(
+        name: String,
+        date: Date,
+        preview: String?,
+        hasUnread: Bool
+    ) {
+        lastDate = date
+        titleLabel.text = name
+        setPreview(string: preview)
+        avatarView.setupGroup(size: .large)
+        unreadView.backgroundColor = hasUnread ? Asset.brandPrimary.color : .clear
     }
 
-    @objc private func longAction(_ sender: UILongPressGestureRecognizer) {
-        if sender.state == .began {
-            didLongPress?()
+    private func setPreview(string: String?) {
+        guard let preview = string else {
+            previewLabel.attributedText = nil
+            return
         }
+
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.lineHeightMultiple = 1.1
+
+        previewLabel.attributedText = NSAttributedString(
+            string: preview,
+            attributes: [
+                .paragraphStyle: paragraphStyle,
+                .font: Fonts.Mulish.regular.font(size: 14.0),
+                .foregroundColor: Asset.neutralSecondaryAlternative.color
+            ])
     }
 }
diff --git a/Sources/ChatListFeature/Views/ChatListContainerView.swift b/Sources/ChatListFeature/Views/ChatListContainerView.swift
new file mode 100644
index 00000000..1be2c99c
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatListContainerView.swift
@@ -0,0 +1,114 @@
+import UIKit
+import Shared
+
+final class ChatSearchListContainerView: UIView{
+    let emptyView = ChatSearchEmptyView()
+
+    init() {
+        super.init(frame: .zero)
+
+        addSubview(emptyView)
+
+        emptyView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+}
+
+final class ChatListContainerView: UIView {
+    let separatorView = UIView()
+    let emptyView = ChatListEmptyView()
+    let collectionContainerView = UIView()
+    lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
+
+    private let layout: UICollectionViewFlowLayout = {
+        let layout = UICollectionViewFlowLayout()
+        layout.minimumLineSpacing = 35
+        layout.itemSize = CGSize(width: 56, height: 80)
+        layout.scrollDirection = .horizontal
+        return layout
+    }()
+
+    init() {
+        super.init(frame: .zero)
+
+        collectionView.showsHorizontalScrollIndicator = false
+        separatorView.backgroundColor = Asset.neutralLine.color
+        collectionView.backgroundColor = Asset.neutralWhite.color
+        collectionView.contentInset = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30)
+
+        addSubview(emptyView)
+        addSubview(collectionContainerView)
+        collectionContainerView.addSubview(collectionView)
+        collectionContainerView.addSubview(separatorView)
+
+        collectionContainerView.snp.makeConstraints {
+            $0.bottom.equalTo(snp.top)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.height.equalTo(110)
+        }
+
+        collectionView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+        }
+
+        separatorView.snp.makeConstraints {
+            $0.top.equalTo(collectionView.snp.bottom).offset(20)
+            $0.height.equalTo(1)
+            $0.left.equalToSuperview().offset(24)
+            $0.right.equalToSuperview().offset(-24)
+            $0.bottom.equalToSuperview()
+        }
+
+        emptyView.snp.makeConstraints {
+            $0.top.equalTo(collectionContainerView.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func showRecentsCollection(_ show: Bool) {
+        if show == true && collectionContainerView.alpha != 0.0 ||
+            show == false && collectionContainerView.alpha == 0.0 {
+            return
+        }
+
+        if show == true {
+            collectionContainerView.alpha = 0.0
+            collectionContainerView.snp.updateConstraints {
+                $0.bottom.equalTo(snp.top).offset(collectionContainerView.bounds.height + 20)
+            }
+
+            UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseInOut) {
+                self.collectionContainerView.alpha = 1.0
+            }
+
+            UIView.animate(withDuration: 0.3, delay: 0.15, options: .curveEaseInOut) {
+                self.setNeedsLayout()
+                self.layoutIfNeeded()
+            }
+        } else {
+            collectionContainerView.alpha = 1.0
+            collectionContainerView.snp.updateConstraints {
+                $0.bottom.equalTo(snp.top)
+            }
+
+            UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseInOut) {
+                self.collectionContainerView.alpha = 0.0
+            }
+
+            UIView.animate(withDuration: 0.2, delay: 0.15, options: .curveEaseInOut) {
+                self.setNeedsLayout()
+                self.layoutIfNeeded()
+            }
+        }
+    }
+}
diff --git a/Sources/ChatListFeature/Views/ChatListEmptyView.swift b/Sources/ChatListFeature/Views/ChatListEmptyView.swift
new file mode 100644
index 00000000..374e1849
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatListEmptyView.swift
@@ -0,0 +1,48 @@
+import UIKit
+import Shared
+
+final class ChatListEmptyView: UIView {
+    private let titleLabel = UILabel()
+    private let stackView = UIStackView()
+    private(set) var contactsButton = CapsuleButton()
+
+    init() {
+        super.init(frame: .zero)
+
+        backgroundColor = Asset.neutralWhite.color
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.lineHeightMultiple = 1.2
+        paragraph.alignment = .center
+
+        titleLabel.numberOfLines = 0
+        titleLabel.attributedText = NSAttributedString(
+            string: Localized.ChatList.emptyTitle,
+            attributes: [
+                .paragraphStyle: paragraph,
+                .foregroundColor: Asset.neutralActive.color,
+                .font: Fonts.Mulish.bold.font(size: 24.0)
+            ]
+        )
+
+        contactsButton.setStyle(.brandColored)
+        contactsButton.setTitle(Localized.ChatList.action, for: .normal)
+
+        stackView.spacing = 24
+        stackView.axis = .vertical
+        stackView.alignment = .center
+        stackView.addArrangedSubview(titleLabel)
+        stackView.addArrangedSubview(contactsButton)
+
+        addSubview(stackView)
+
+        stackView.snp.makeConstraints {
+            $0.centerY.equalToSuperview()
+            $0.top.greaterThanOrEqualToSuperview()
+            $0.left.equalToSuperview().offset(24)
+            $0.right.equalToSuperview().offset(-24)
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift
new file mode 100644
index 00000000..4adbdeef
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatListRecentContactCell.swift
@@ -0,0 +1,89 @@
+import UIKit
+import Shared
+
+final class ChatListRecentContactCell: UICollectionViewCell {
+    private let titleLabel = UILabel()
+    private let containerView = UIView()
+    private let avatarView = AvatarView()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+
+        contentView.backgroundColor = .white
+
+        let newLabel = UILabel()
+        newLabel.text = "NEW"
+        newLabel.textColor = Asset.neutralWhite.color
+        newLabel.font = Fonts.Mulish.bold.font(size: 8.0)
+
+        let newContainerView = UIView()
+        newContainerView.layer.cornerRadius = 6.0
+        newContainerView.layer.masksToBounds = true
+        newContainerView.backgroundColor = Asset.accentSafe.color
+
+        titleLabel.numberOfLines = 2
+        titleLabel.textAlignment = .center
+        titleLabel.lineBreakMode = .byWordWrapping
+        titleLabel.textColor = Asset.neutralDark.color
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
+
+        contentView.addSubview(titleLabel)
+        contentView.addSubview(containerView)
+
+        containerView.addSubview(avatarView)
+        containerView.addSubview(newContainerView)
+
+        newContainerView.addSubview(newLabel)
+
+        containerView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+        }
+
+        newContainerView.snp.makeConstraints {
+            $0.top.equalTo(containerView.snp.top)
+            $0.right.equalTo(containerView.snp.right)
+        }
+
+        newLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(3)
+            $0.center.equalToSuperview()
+            $0.left.equalToSuperview().offset(3)
+        }
+
+        avatarView.snp.makeConstraints {
+            $0.width.equalTo(48)
+            $0.height.equalTo(48)
+            $0.top.equalToSuperview().offset(4)
+            $0.left.equalToSuperview().offset(4)
+            $0.right.equalToSuperview().offset(-4)
+            $0.bottom.equalToSuperview().offset(-4)
+        }
+
+        titleLabel.snp.makeConstraints {
+            $0.centerX.equalToSuperview()
+            $0.top.equalTo(containerView.snp.bottom).offset(5)
+            $0.left.greaterThanOrEqualToSuperview()
+            $0.right.lessThanOrEqualToSuperview()
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        titleLabel.text = nil
+        avatarView.prepareForReuse()
+    }
+
+    func setup(title: String, image: Data?) {
+        titleLabel.text = title
+        avatarView.setupProfile(
+            title: title,
+            image: image,
+            size: .large
+        )
+    }
+}
diff --git a/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift
new file mode 100644
index 00000000..6cc8a78d
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatListTopLeftNavView.swift
@@ -0,0 +1,72 @@
+import UIKit
+import Shared
+import Combine
+
+final class ChatListTopLeftNavView: UIView {
+    private let titleLabel = UILabel()
+    private let badgeLabel = UILabel()
+    private let menuButton = UIButton()
+    private let stackView = UIStackView()
+    private let badgeContainerView = UIView()
+
+    var actionPublisher: AnyPublisher<Void, Never> {
+        actionSubject.eraseToAnyPublisher()
+    }
+
+    private let actionSubject = PassthroughSubject<Void, Never>()
+
+    init() {
+        super.init(frame: .zero)
+
+        titleLabel.text = Localized.ChatList.title
+        titleLabel.textColor = Asset.neutralActive.color
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
+
+        menuButton.tintColor = Asset.neutralDark.color
+        menuButton.setImage(Asset.chatListMenu.image, for: .normal)
+        menuButton.addTarget(self, action: #selector(didTapMenu), for: .touchUpInside)
+
+        badgeLabel.textColor = Asset.neutralWhite.color
+        badgeLabel.font = Fonts.Mulish.bold.font(size: 12.0)
+
+        badgeContainerView.layer.cornerRadius = 5
+        badgeContainerView.layer.masksToBounds = true
+        badgeContainerView.backgroundColor = Asset.brandPrimary.color
+
+        badgeContainerView.addSubview(badgeLabel)
+        menuButton.addSubview(badgeContainerView)
+        stackView.addArrangedSubview(menuButton)
+        stackView.addArrangedSubview(titleLabel)
+        addSubview(stackView)
+
+        badgeLabel.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(3)
+            $0.center.equalToSuperview()
+            $0.left.equalToSuperview().offset(3)
+        }
+
+        badgeContainerView.snp.makeConstraints {
+            $0.centerY.equalTo(menuButton.snp.top)
+            $0.centerX.equalTo(menuButton.snp.right).multipliedBy(0.8)
+        }
+
+        menuButton.snp.makeConstraints { $0.width.equalTo(50) }
+        stackView.snp.makeConstraints { $0.edges.equalToSuperview() }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    @objc private func didTapMenu() {
+        actionSubject.send()
+    }
+
+    func updateBadge(_ count: Int) {
+        guard count > 0 else {
+            badgeContainerView.isHidden = true
+            return
+        }
+
+        badgeLabel.text = "\(count)"
+        badgeContainerView.isHidden = false
+    }
+}
diff --git a/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift
new file mode 100644
index 00000000..817893e1
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatListTopRightNavView.swift
@@ -0,0 +1,49 @@
+import UIKit
+import Shared
+import Combine
+
+final class ChatListTopRightNavView: UIView {
+    enum Action {
+        case didTapSearch
+        case didTapNewGroup
+    }
+
+    var actionPublisher: AnyPublisher<Action, Never> {
+        actionSubject.eraseToAnyPublisher()
+    }
+
+    private let stackView = UIStackView()
+    private let searchButton = UIButton()
+    private let newGroupButton = UIButton()
+    private let actionSubject = PassthroughSubject<Action, Never>()
+
+    init() {
+        super.init(frame: .zero)
+
+        searchButton.tintColor = Asset.neutralDark.color
+        newGroupButton.tintColor = Asset.neutralDark.color
+        searchButton.setImage(Asset.chatListUd.image, for: .normal)
+        newGroupButton.setImage(Asset.chatListNewGroup.image, for: .normal)
+        searchButton.addTarget(self, action: #selector(didTapSearch), for: .touchUpInside)
+        newGroupButton.addTarget(self, action: #selector(didTapNewGroup), for: .touchUpInside)
+
+        stackView.spacing = 10
+        stackView.addArrangedSubview(newGroupButton)
+        stackView.addArrangedSubview(searchButton)
+        addSubview(stackView)
+
+        searchButton.snp.makeConstraints { $0.width.equalTo(40) }
+        newGroupButton.snp.makeConstraints { $0.width.equalTo(40) }
+        stackView.snp.makeConstraints { $0.edges.equalToSuperview() }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    @objc private func didTapSearch() {
+        actionSubject.send(.didTapSearch)
+    }
+
+    @objc private func didTapNewGroup() {
+        actionSubject.send(.didTapNewGroup)
+    }
+}
diff --git a/Sources/ChatListFeature/Views/ChatListView.swift b/Sources/ChatListFeature/Views/ChatListView.swift
index 918cffcc..c7303c48 100644
--- a/Sources/ChatListFeature/Views/ChatListView.swift
+++ b/Sources/ChatListFeature/Views/ChatListView.swift
@@ -2,93 +2,75 @@ import UIKit
 import Shared
 
 final class ChatListView: UIView {
-    let titleLabel = UILabel()
     let snackBar = SnackBar()
-    let stackView = UIStackView()
-    let contactsButton = CapsuleButton()
+    let containerView = UIView()
     let searchView = SearchComponent()
-
-    var networkIssueVisibleConstraint: NSLayoutConstraint?
-    var networkIssueInvisibleConstraint: NSLayoutConstraint?
+    let listContainerView = ChatListContainerView()
+    let searchListContainerView = ChatSearchListContainerView()
 
     init() {
         super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    func displayNetworkIssue(_ flag: Bool) {
-        self.networkIssueInvisibleConstraint?.isActive = !flag
-        self.networkIssueVisibleConstraint?.isActive = flag
-
-        snackBar.alpha = flag ? 0 : 1
-
-        UIView.animate(withDuration: 0.5) {
-            self.setNeedsLayout()
-            self.layoutIfNeeded()
-            self.snackBar.alpha = flag ? 1 : 0
-        }
-    }
 
-    private func setup() {
-        snackBar.alpha = 0.0
         backgroundColor = Asset.neutralWhite.color
-
-        let paragraph = NSMutableParagraphStyle()
-        paragraph.lineHeightMultiple = 1.2
-        paragraph.alignment = .center
-
-        titleLabel.numberOfLines = 0
-        titleLabel.attributedText = NSAttributedString(
-            string: Localized.ChatList.emptyTitle,
-            attributes: [
-                .paragraphStyle: paragraph,
-                .foregroundColor: Asset.neutralActive.color,
-                .font: Fonts.Mulish.bold.font(size: 24.0)
-            ]
-        )
-
-        contactsButton.setStyle(.brandColored)
-        contactsButton.setTitle(Localized.ChatList.action, for: .normal)
-
+        listContainerView.backgroundColor = Asset.neutralWhite.color
+        searchListContainerView.backgroundColor = Asset.neutralWhite.color
         searchView.update(placeholder: "Search chats")
 
-        stackView.spacing = 24
-        stackView.axis = .vertical
-        stackView.alignment = .center
-        stackView.addArrangedSubview(titleLabel)
-        stackView.addArrangedSubview(contactsButton)
-
         addSubview(snackBar)
         addSubview(searchView)
-        addSubview(stackView)
+        addSubview(containerView)
+        containerView.addSubview(searchListContainerView)
+        containerView.addSubview(listContainerView)
+
+        snackBar.snp.makeConstraints {
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalTo(snp.top)
+        }
 
-        setupConstraints()
-    }
+        searchView.snp.makeConstraints {
+            $0.top.equalTo(snackBar.snp.bottom).offset(20)
+            $0.left.equalToSuperview().offset(20)
+            $0.right.equalToSuperview().offset(-20)
+        }
 
-    private func setupConstraints() {
-        NSLayoutConstraint.activate([
-            snackBar.leftAnchor.constraint(equalTo: leftAnchor),
-            snackBar.rightAnchor.constraint(equalTo: rightAnchor)
-        ])
+        containerView.snp.makeConstraints {
+            $0.top.equalTo(searchView.snp.bottom)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
 
-        networkIssueVisibleConstraint = snackBar.topAnchor.constraint(equalTo: topAnchor)
-        networkIssueInvisibleConstraint = snackBar.bottomAnchor.constraint(equalTo: topAnchor)
+        listContainerView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
 
-        networkIssueInvisibleConstraint?.isActive = true
-        snackBar.translatesAutoresizingMaskIntoConstraints = false
+        searchListContainerView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
 
-        searchView.snp.makeConstraints { make in
-            make.top.equalTo(snackBar.snp.bottom).offset(20)
-            make.left.equalToSuperview().offset(20)
-            make.right.equalToSuperview().offset(-20)
+    func showConnectingBanner(_ show: Bool) {
+        if show == true {
+            snackBar.alpha = 0.0
+            snackBar.snp.updateConstraints {
+                $0.bottom
+                    .equalTo(snp.top)
+                    .offset(snackBar.bounds.height)
+            }
+        } else {
+            snackBar.alpha = 1.0
+            snackBar.snp.updateConstraints {
+                $0.bottom.equalTo(snp.top)
+            }
         }
 
-        stackView.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.left.equalToSuperview().offset(24)
-            make.right.equalToSuperview().offset(-24)
+        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
+            self.setNeedsLayout()
+            self.layoutIfNeeded()
+            self.snackBar.alpha = show ? 1.0 : 0.0
         }
     }
 }
diff --git a/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift
new file mode 100644
index 00000000..2fe1471c
--- /dev/null
+++ b/Sources/ChatListFeature/Views/ChatSearchEmptyView.swift
@@ -0,0 +1,57 @@
+import UIKit
+import Shared
+
+final class ChatSearchEmptyView: UIView {
+    private let titleLabel = UILabel()
+    private let stackView = UIStackView()
+    private let descriptionLabel = UILabel()
+    private(set) var searchButton = CapsuleButton()
+
+    init() {
+        super.init(frame: .zero)
+        backgroundColor = Asset.neutralWhite.color
+
+        titleLabel.textColor = Asset.brandPrimary.color
+        titleLabel.font = Fonts.Mulish.bold.font(size: 24.0)
+
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.lineHeightMultiple = 1.2
+
+        descriptionLabel.numberOfLines = 0
+        descriptionLabel.attributedText = NSAttributedString(
+            string: "was not found in your connections or in a chat. Click below to search for them as a new connection.",
+            attributes: [
+                .paragraphStyle: paragraph,
+                .foregroundColor: Asset.neutralActive.color,
+                .font: Fonts.Mulish.regular.font(size: 16.0)
+            ]
+        )
+
+        searchButton.setStyle(.brandColored)
+        searchButton.setTitle("Search for a connection", for: .normal)
+
+        stackView.axis = .vertical
+        stackView.addArrangedSubview(titleLabel)
+        stackView.addArrangedSubview(descriptionLabel)
+        stackView.addArrangedSubview(searchButton)
+
+        stackView.setCustomSpacing(10, after: titleLabel)
+        stackView.setCustomSpacing(30, after: descriptionLabel)
+
+        addSubview(stackView)
+
+        stackView.snp.makeConstraints {
+            $0.centerY.equalToSuperview().multipliedBy(0.5)
+            $0.top.greaterThanOrEqualToSuperview()
+            $0.left.equalToSuperview().offset(24)
+            $0.right.equalToSuperview().offset(-24)
+            $0.bottom.lessThanOrEqualToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func updateSearched(content: String) {
+        titleLabel.text = content
+    }
+}
diff --git a/Sources/Database/DB+Contact.swift b/Sources/Database/DB+Contact.swift
index e8771ab9..4207fea7 100644
--- a/Sources/Database/DB+Contact.swift
+++ b/Sources/Database/DB+Contact.swift
@@ -10,6 +10,7 @@ extension Contact: Persistable {
         case userId
         case status
         case username
+        case isRecent
         case nickname
         case marshaled
         case createdAt
@@ -23,6 +24,10 @@ extension Contact: Persistable {
         switch request {
         case .all:
             return Contact.all()
+        case .isRecent:
+            return Contact
+                .filter(Column.isRecent == true)
+                .order(Column.createdAt.desc)
         case .verificationInProgress:
             return Contact.filter(Column.status == Contact.Status.verificationInProgress.rawValue)
         case .failed:
diff --git a/Sources/Database/DB+GroupChatInfo.swift b/Sources/Database/DB+GroupChatInfo.swift
index 233b8952..4be4dfad 100644
--- a/Sources/Database/DB+GroupChatInfo.swift
+++ b/Sources/Database/DB+GroupChatInfo.swift
@@ -17,6 +17,15 @@ extension GroupChatInfo: Requestable  {
         }.order(GroupMessage.Column.timestamp.desc)
 
         switch request {
+        case .fromGroup(let groupId):
+            return Group
+                .filter(Group.Column.status == Group.Status.participating.rawValue)
+                .filter(Group.Column.groupId == groupId)
+                .with(lastMessageCTE)
+                .including(optional: lastMessage)
+                .including(all: Group.members.forKey("members"))
+                .asRequest(of: Self.self)
+
         case .accepted:
             return Group
                 .filter(Group.Column.status == Group.Status.participating.rawValue)
diff --git a/Sources/Database/DatabaseManager.swift b/Sources/Database/DatabaseManager.swift
index db9d5565..86271c2e 100644
--- a/Sources/Database/DatabaseManager.swift
+++ b/Sources/Database/DatabaseManager.swift
@@ -115,15 +115,26 @@ extension GRDBDatabaseManager: DatabaseManager {
     public func setup() throws {
         var migrator = DatabaseMigrator()
 
-        let path = NSSearchPathForDirectoriesInDomains(
-            .documentDirectory, .userDomainMask, true
-        )[0]
-        .appending("/xxmessenger.sqlite")
+        let oldPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
+            .appending("/xxmessenger.sqlite")
+
+        let url = FileManager.default
+            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
+            .appendingPathComponent("database")
+            .appendingPathExtension("sqlite")
+
+        if FileManager.default.fileExists(atPath: oldPath) && !FileManager.default.fileExists(atPath: url.path) {
+            do {
+                try FileManager.default.moveItem(atPath: oldPath, toPath: url.path)
+            } catch {
+                fatalError("Couldn't migrate database from old path to new one: \(error.localizedDescription)")
+            }
+        }
 
-        databaseQueue = try DatabaseQueue(path: path)
+        databaseQueue = try DatabaseQueue(path: url.path)
         try FileManager.default.setAttributes([
             .protectionKey : FileProtectionType.completeUntilFirstUserAuthentication
-        ], ofItemAtPath: path)
+        ], ofItemAtPath: url.path)
 
         migrator.registerMigration("v1") { db in
             try db.create(table: Contact.databaseTableName, ifNotExists: true) { table in
@@ -237,6 +248,14 @@ extension GRDBDatabaseManager: DatabaseManager {
             try db.rename(table: "temp_\(Group.databaseTableName)", to: Group.databaseTableName)
         }
 
+        migrator.registerMigration("v2") { db in
+            try db.alter(table: Contact.databaseTableName) { table in
+                table.add(column: Contact.Column.isRecent.rawValue, .boolean)
+            }
+
+            try Contact.updateAll(db, Contact.Column.isRecent.set(to: false))
+        }
+
         try migrator.migrate(databaseQueue)
     }
 }
diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift
index 51bdf9d3..bdd6784d 100644
--- a/Sources/Integration/Client.swift
+++ b/Sources/Integration/Client.swift
@@ -225,7 +225,7 @@ public class Client {
     }
 
     private func updatePreImage() {
-        if let defaults = UserDefaults(suiteName: "group.io.xxlabs.notification") {
+        if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") {
             defaults.set(bindings.getPreImages(), forKey: "preImage")
         }
     }
diff --git a/Sources/Integration/Extensions.swift b/Sources/Integration/Extensions.swift
index 5cfa63f0..93cf186a 100644
--- a/Sources/Integration/Extensions.swift
+++ b/Sources/Integration/Extensions.swift
@@ -12,7 +12,8 @@ extension Contact {
             marshaled: try! contact.marshal(),
             username: contact.retrieve(fact: .username) ?? "",
             nickname: nil,
-            createdAt: Date()
+            createdAt: Date(),
+            isRecent: false
         )
     }
 }
diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift
index c0ab7acf..ce843f4a 100644
--- a/Sources/Integration/Implementations/Bindings.swift
+++ b/Sources/Integration/Implementations/Bindings.swift
@@ -9,6 +9,7 @@ public let evaluateNotification: NotificationEvaluation = BindingsNotificationsF
 public protocol NotificationReportProtocol {
     func forMe() -> Bool
     func type() -> String
+    func source() -> Data?
 }
 
 public protocol NotificationManyReportProtocol {
@@ -369,22 +370,12 @@ extension BindingsClient: BindingsInterface {
         }
     }
 
-    /// Registers device token on backend for push notifications
-    ///
-    /// - Parameters:
-    ///   - string: Device token provided by APNS
-    ///
-    /// - Throws: If an exception was raised on
-    ///           backend such as timing out
-    ///
-    public func registerNotifications(_ token: String) throws {
-        log(type: .crumbs)
+    public func registerNotifications(_ token: Data) throws {
+        let tokenString = token.map { String(format: "%02hhx", $0) }.joined()
 
         do {
-            try register(forNotifications: token)
-            log(string: "Registered for notifications using token: \(token)", type: .info)
+            try register(forNotifications: tokenString)
         } catch {
-            log(string: error.localizedDescription, type: .error)
             throw error.friendly()
         }
     }
diff --git a/Sources/Integration/Interfaces/BindingsInterface.swift b/Sources/Integration/Interfaces/BindingsInterface.swift
index ed5b3077..34e0e420 100644
--- a/Sources/Integration/Interfaces/BindingsInterface.swift
+++ b/Sources/Integration/Interfaces/BindingsInterface.swift
@@ -112,7 +112,7 @@ public protocol BindingsInterface {
 
     func getPreImages() -> String
 
-    func registerNotifications(_: String) throws
+    func registerNotifications(_ token: Data) throws
 
     func unregisterNotifications() throws
 
diff --git a/Sources/Integration/Listeners.swift b/Sources/Integration/Listeners.swift
index f820cb04..23f9f223 100644
--- a/Sources/Integration/Listeners.swift
+++ b/Sources/Integration/Listeners.swift
@@ -5,6 +5,10 @@ import Foundation
 import os.log
 import Combine
 
+import Combine
+
+import Combine
+
 public extension BindingsClient {
     static func listenLogs() {
         let callback = LogCallback { log(string: $0 ?? "", type: .bindings) }
@@ -13,7 +17,7 @@ public extension BindingsClient {
 
     func listenPreImageUpdates() {
         let callback = PreImageCallback { [weak self] _, _ in
-            if let defaults = UserDefaults(suiteName: "group.io.xxlabs.notification") {
+            if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") {
                 let preImage = self?.getPreImages()
                 defaults.set(preImage, forKey: "preImage")
             }
diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift
index 1235f040..8eaf8a06 100644
--- a/Sources/Integration/Mocks/BindingsMock.swift
+++ b/Sources/Integration/Mocks/BindingsMock.swift
@@ -64,7 +64,7 @@ public final class BindingsMock: BindingsInterface {
 
     public func unregisterNotifications() throws {}
 
-    public func registerNotifications(_: String) throws {}
+    public func registerNotifications(_: Data) throws {}
 
     public func compress(image: Data, _: @escaping(Result<Data, Error>) -> Void) {}
 
@@ -85,6 +85,7 @@ public final class BindingsMock: BindingsInterface {
 
     public func listenMessages(_: @escaping (Message) -> Void) throws {}
 
+
     public func initializeBackup(
         passphrase: String,
         callback: @escaping (Data) -> Void
@@ -94,6 +95,8 @@ public final class BindingsMock: BindingsInterface {
         callback: @escaping (Data) -> Void
     ) -> BackupInterface { BindingsBackupMock() }
 
+    public func listenBackups(_: @escaping (Data) -> Void) -> BackupInterface { fatalError() }
+
     public func listenNetworkUpdates(_: @escaping (Bool) -> Void) {}
 
     public func confirm(_: Data, _ completion: @escaping (Result<Bool, Error>) -> Void) {
@@ -207,7 +210,8 @@ extension Contact {
                 marshaled: "brad\(n)".data(using: .utf8)!,
                 username: "brad\(n)",
                 nickname: nil,
-                createdAt: Date()
+                createdAt: Date(),
+                isRecent: false
             ))
         }
 
@@ -223,7 +227,8 @@ extension Contact {
         marshaled: "angelinajolie".data(using: .utf8)!,
         username: "angelinajolie",
         nickname: "Angelica Jolie",
-        createdAt: Date()
+        createdAt: Date(),
+        isRecent: false
     )
 
     static let carlRequested = Contact(
@@ -235,7 +240,8 @@ extension Contact {
         marshaled: "carlsagan".data(using: .utf8)!,
         username: "carlsagan",
         nickname: "Carl Sagan",
-        createdAt: Date.distantPast
+        createdAt: Date.distantPast,
+        isRecent: false
     )
 
     static let elonRequested = Contact(
@@ -247,7 +253,8 @@ extension Contact {
         marshaled: "elonmusk".data(using: .utf8)!,
         username: "elonmusk",
         nickname: "Elon Musk",
-        createdAt: Date.distantPast
+        createdAt: Date.distantPast,
+        isRecent: false
     )
 
     static let georgeDiscovered = Contact(
@@ -259,7 +266,8 @@ extension Contact {
         marshaled: "georgebenson74".data(using: .utf8)!,
         username: "bruno_muniz74",
         nickname: "Bruno Muniz",
-        createdAt: Date()
+        createdAt: Date(),
+        isRecent: false
     )
 }
 
diff --git a/Sources/Integration/Mocks/UserDiscoveryMock.swift b/Sources/Integration/Mocks/UserDiscoveryMock.swift
index 69cd094e..910faf08 100644
--- a/Sources/Integration/Mocks/UserDiscoveryMock.swift
+++ b/Sources/Integration/Mocks/UserDiscoveryMock.swift
@@ -35,7 +35,8 @@ final class UserDiscoveryMock: UserDiscoveryInterface {
                 marshaled: "mock_username".data(using: .utf8)!,
                 username: "mock_username",
                 nickname: "mock_nickname",
-                createdAt: Date()
+                createdAt: Date(),
+                isRecent: false
             )))
         }
     }
diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift
index 1c6690b5..53204708 100644
--- a/Sources/Integration/Session/Session+Contacts.swift
+++ b/Sources/Integration/Session/Session+Contacts.swift
@@ -198,6 +198,8 @@ extension Session {
 
             switch $0 {
             case .success(let confirmed):
+                contact.isRecent = true
+                contact.createdAt = Date()
                 contact.status = confirmed ? .friend : .confirmationFailed
                 log(string: "Confirming request from \(title) = \(confirmed)", type: confirmed ? .info : .error)
             case .failure(let error):
@@ -216,6 +218,8 @@ extension Session {
                 stored.photo = contact.photo
                 stored.phone = contact.phone
                 stored.nickname = contact.nickname
+                stored.isRecent = contact.isRecent
+                stored.createdAt = contact.createdAt
                 try dbManager.save(stored)
 
                 try dbManager.updateAll(
diff --git a/Sources/Integration/Session/Session+Notifications.swift b/Sources/Integration/Session/Session+Notifications.swift
index 3eea889c..b4e98e96 100644
--- a/Sources/Integration/Session/Session+Notifications.swift
+++ b/Sources/Integration/Session/Session+Notifications.swift
@@ -1,6 +1,8 @@
+import Foundation
+
 extension Session {
-    public func registerNotifications(_ string: String) throws {
-        try client.bindings.registerNotifications(string)
+    public func registerNotifications(_ token: Data) throws {
+        try client.bindings.registerNotifications(token)
     }
 
     public func unregisterNotifications() throws {
diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift
index 7e0f40d9..2982b295 100644
--- a/Sources/Integration/Session/Session.swift
+++ b/Sources/Integration/Session/Session.swift
@@ -380,8 +380,14 @@ public final class Session: SessionType {
             .store(in: &cancellables)
 
         client.messages
-            .sink { [unowned self] in _ = try? dbManager.save($0) }
-            .store(in: &cancellables)
+            .sink { [unowned self] in
+                if var contact: Contact = try? dbManager.fetch(.withUserId($0.sender)).first {
+                    contact.isRecent = false
+                    _ = try? dbManager.save(contact)
+                }
+
+                _ = try? dbManager.save($0)
+            }.store(in: &cancellables)
 
         client.network
             .sink { [unowned self] in networkMonitor.update($0) }
@@ -404,6 +410,8 @@ public final class Session: SessionType {
             .sink { [unowned self] in
                 if var contact: Contact = try? dbManager.fetch(.withUserId($0.userId)).first {
                     contact.status = .friend
+                    contact.isRecent = true
+                    contact.createdAt = Date()
                     _ = try? dbManager.save(contact)
 
                     toastController.enqueueToast(model: .init(
@@ -424,4 +432,14 @@ public final class Session: SessionType {
         guard let message: GroupMessage = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil }
         return message.payload.text
     }
+
+    public func getContactWith(userId: Data) -> Contact? {
+        let contact: Contact? = try? dbManager.fetch(.withUserId(userId)).first
+        return contact
+    }
+
+    public func getGroupChatInfoWith(groupId: Data) -> GroupChatInfo? {
+        let info: GroupChatInfo? = try? dbManager.fetch(.fromGroup(groupId)).first
+        return info
+    }
 }
diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift
index a1e2c733..12146949 100644
--- a/Sources/Integration/Session/SessionType.swift
+++ b/Sources/Integration/Session/SessionType.swift
@@ -46,7 +46,7 @@ public protocol SessionType {
     // Notifications
 
     func unregisterNotifications() throws
-    func registerNotifications(_ string: String) throws
+    func registerNotifications(_ token: Data) throws
 
     // Network
 
@@ -93,4 +93,7 @@ public protocol SessionType {
         members: [Contact],
         _ completion: @escaping (Result<(Group, [GroupMember]), Error>) -> Void
     )
+
+    func getContactWith(userId: Data) -> Contact?
+    func getGroupChatInfoWith(groupId: Data) -> GroupChatInfo?
 }
diff --git a/Sources/Integration/XXNetwork.swift b/Sources/Integration/XXNetwork.swift
index 34d656ec..1273a26a 100644
--- a/Sources/Integration/XXNetwork.swift
+++ b/Sources/Integration/XXNetwork.swift
@@ -143,7 +143,7 @@ extension XXNetwork: XXNetworking {
         let bindings = B.login(FileManager.xxPath, secret, "", &error)
         if let error = error { throw error }
 
-        if let defaults = UserDefaults(suiteName: "group.io.xxlabs.notification") {
+        if let defaults = UserDefaults(suiteName: "group.elixxir.messenger") {
             defaults.set(bindings!.receptionId.base64EncodedString(), forKey: "receptionId")
         }
 
diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift
new file mode 100644
index 00000000..33c2f8da
--- /dev/null
+++ b/Sources/LaunchFeature/LaunchController.swift
@@ -0,0 +1,152 @@
+import HUD
+import UIKit
+import Shared
+import Combine
+import PushFeature
+import DependencyInjection
+
+
+public final class LaunchController: UIViewController {
+    @Dependency private var hud: HUDType
+    @Dependency private var coordinator: LaunchCoordinating
+
+    lazy private var screenView = LaunchView()
+
+    private let blocker = UpdateBlocker()
+    private let viewModel = LaunchViewModel()
+    public var pendingPushRoute: PushRouter.Route?
+    private var cancellables = Set<AnyCancellable>()
+
+    public override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        viewModel.viewDidAppear()
+    }
+
+    public override func loadView() {
+        view = screenView
+    }
+
+    public override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        navigationController?
+            .navigationBar
+            .customize(translucent: true)
+    }
+
+    public override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+        screenView.setupGradient()
+    }
+
+    public override func viewDidLoad() {
+        super.viewDidLoad()
+
+        viewModel.hudPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [hud] in hud.update(with: $0) }
+            .store(in: &cancellables)
+
+        viewModel.routePublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                switch $0 {
+                case .chats:
+                    if let pushRoute = pendingPushRoute {
+                        switch pushRoute {
+                        case .requests:
+                            coordinator.toRequests(from: self)
+
+                        case .groupChat(id: let groupId):
+                            if let groupInfo = viewModel.getGroupInfoWith(groupId: groupId) {
+                                coordinator.toGroupChat(with: groupInfo, from: self)
+                                return
+                            }
+                            coordinator.toChats(from: self)
+
+                        case .contactChat(id: let userId):
+                            if let contact = viewModel.getContactWith(userId: userId) {
+                                coordinator.toSingleChat(with: contact, from: self)
+                                return
+                            }
+                            coordinator.toChats(from: self)
+                        }
+
+                        return
+                    }
+
+                    coordinator.toChats(from: self)
+
+                case .onboarding(let ndf):
+                    coordinator.toOnboarding(with: ndf, from: self)
+
+                case .update(let model):
+                    offerUpdate(model: model)
+                }
+            }.store(in: &cancellables)
+    }
+
+    private func offerUpdate(model: Update) {
+        let drawerView = UIView()
+        drawerView.backgroundColor = Asset.neutralSecondary.color
+        drawerView.layer.cornerRadius = 5
+
+        let vStack = UIStackView()
+        vStack.axis = .vertical
+        vStack.spacing = 10
+        drawerView.addSubview(vStack)
+
+        vStack.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(18)
+            $0.left.equalToSuperview().offset(18)
+            $0.right.equalToSuperview().offset(-18)
+            $0.bottom.equalToSuperview().offset(-18)
+        }
+
+        let title = UILabel()
+        title.text = "App Update"
+        title.textAlignment = .center
+        title.textColor = Asset.neutralDark.color
+
+        let body = UILabel()
+        body.numberOfLines = 0
+        body.textAlignment = .center
+        body.textColor = Asset.neutralDark.color
+
+        let update = CapsuleButton()
+        update.publisher(for: .touchUpInside)
+            .sink { UIApplication.shared.open(.init(string: model.urlString)!, options: [:]) }
+            .store(in: &cancellables)
+
+        vStack.addArrangedSubview(title)
+        vStack.addArrangedSubview(body)
+        vStack.addArrangedSubview(update)
+
+        body.text = model.content
+        update.set(
+            style: model.actionStyle,
+            title: model.positiveActionTitle
+        )
+
+        if let negativeTitle = model.negativeActionTitle {
+            let negativeButton = CapsuleButton()
+            negativeButton.set(style: .simplestColoredRed, title: negativeTitle)
+
+            negativeButton.publisher(for: .touchUpInside)
+                .sink { [unowned self] in
+                    blocker.hideWindow()
+                    viewModel.versionApproved()
+                }.store(in: &cancellables)
+
+            vStack.addArrangedSubview(negativeButton)
+        }
+
+        blocker.window?.addSubview(drawerView)
+        drawerView.snp.makeConstraints {
+            $0.left.equalToSuperview().offset(18)
+            $0.center.equalToSuperview()
+            $0.right.equalToSuperview().offset(-18)
+        }
+
+        blocker.showWindow()
+    }
+}
diff --git a/Sources/LaunchFeature/LaunchCoordinator.swift b/Sources/LaunchFeature/LaunchCoordinator.swift
new file mode 100644
index 00000000..2f446810
--- /dev/null
+++ b/Sources/LaunchFeature/LaunchCoordinator.swift
@@ -0,0 +1,64 @@
+import UIKit
+import Models
+import Presentation
+
+public protocol LaunchCoordinating {
+    func toChats(from: UIViewController)
+    func toRequests(from: UIViewController)
+    func toOnboarding(with: String, from: UIViewController)
+    func toSingleChat(with: Contact, from: UIViewController)
+    func toGroupChat(with: GroupChatInfo, from: UIViewController)
+}
+
+public struct LaunchCoordinator: LaunchCoordinating {
+    var replacePresenter: Presenting = ReplacePresenter()
+
+    var requestsFactory: () -> UIViewController
+    var chatListFactory: () -> UIViewController
+    var onboardingFactory: (String) -> UIViewController
+    var singleChatFactory: (Contact) -> UIViewController
+    var groupChatFactory: (GroupChatInfo) -> UIViewController
+
+    public init(
+        requestsFactory: @escaping () -> UIViewController,
+        chatListFactory: @escaping () -> UIViewController,
+        onboardingFactory: @escaping (String) -> UIViewController,
+        singleChatFactory: @escaping (Contact) -> UIViewController,
+        groupChatFactory: @escaping (GroupChatInfo) -> UIViewController
+    ) {
+        self.requestsFactory = requestsFactory
+        self.chatListFactory = chatListFactory
+        self.groupChatFactory = groupChatFactory
+        self.onboardingFactory = onboardingFactory
+        self.singleChatFactory = singleChatFactory
+    }
+}
+
+public extension LaunchCoordinator {
+    func toChats(from parent: UIViewController) {
+        let screen = chatListFactory()
+        replacePresenter.present(screen, from: parent)
+    }
+
+    func toRequests(from parent: UIViewController) {
+        let screen = requestsFactory()
+        replacePresenter.present(screen, from: parent)
+    }
+
+    func toOnboarding(with ndf: String, from parent: UIViewController) {
+        let screen = onboardingFactory(ndf)
+        replacePresenter.present(screen, from: parent)
+    }
+
+    func toSingleChat(with contact: Contact, from parent: UIViewController) {
+        let chatListScreen = chatListFactory()
+        let singleChatScreen = singleChatFactory(contact)
+        replacePresenter.present(chatListScreen, singleChatScreen, from: parent)
+    }
+
+    func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) {
+        let chatListScreen = chatListFactory()
+        let groupChatScreen = groupChatFactory(group)
+        replacePresenter.present(chatListScreen, groupChatScreen, from: parent)
+    }
+}
diff --git a/Sources/LaunchFeature/LaunchView.swift b/Sources/LaunchFeature/LaunchView.swift
new file mode 100644
index 00000000..995c9f8c
--- /dev/null
+++ b/Sources/LaunchFeature/LaunchView.swift
@@ -0,0 +1,37 @@
+import UIKit
+import Shared
+
+final class LaunchView: UIView {
+    private var imageView = UIImageView()
+
+    init() {
+        super.init(frame: .zero)
+        imageView.image = Asset.splash.image
+        imageView.contentMode = .scaleAspectFit
+        backgroundColor = Asset.neutralWhite.color
+
+        addSubview(imageView)
+
+        imageView.snp.makeConstraints {
+            $0.center.equalToSuperview()
+            $0.left.equalToSuperview().offset(100)
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func setupGradient() {
+        let gradient = CAGradientLayer()
+        gradient.colors = [
+            UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor,
+            UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor,
+            UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor,
+            UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor
+        ]
+
+        gradient.frame = bounds
+        gradient.startPoint = CGPoint(x: 1, y: 0)
+        gradient.endPoint = CGPoint(x: 0, y: 1)
+        layer.insertSublayer(gradient, at: 0)
+    }
+}
diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift
new file mode 100644
index 00000000..5e1b78a6
--- /dev/null
+++ b/Sources/LaunchFeature/LaunchViewModel.swift
@@ -0,0 +1,190 @@
+import HUD
+import Shared
+import Models
+import Combine
+import Defaults
+import Foundation
+import Integration
+import Permissions
+import DropboxFeature
+import VersionChecking
+import CombineSchedulers
+import DependencyInjection
+
+struct Update {
+    let content: String
+    let urlString: String
+    let positiveActionTitle: String
+    let negativeActionTitle: String?
+    let actionStyle: CapsuleButtonStyle
+}
+
+enum LaunchRoute {
+    case chats
+    case update(Update)
+    case onboarding(String)
+}
+
+final class LaunchViewModel {
+    @Dependency private var network: XXNetworking
+    @Dependency private var versionChecker: VersionChecker
+    @Dependency private var dropboxService: DropboxInterface
+    @Dependency private var permissionHandler: PermissionHandling
+
+    @KeyObject(.username, defaultValue: nil) var username: String?
+    @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool
+
+    var hudPublisher: AnyPublisher<HUDStatus, Never> {
+        hudSubject.eraseToAnyPublisher()
+    }
+
+    var routePublisher: AnyPublisher<LaunchRoute, Never> {
+        routeSubject.eraseToAnyPublisher()
+    }
+
+    var backgroundScheduler: AnySchedulerOf<DispatchQueue> = {
+        DispatchQueue.global().eraseToAnyScheduler()
+    }()
+
+    var getSession: (String) throws -> SessionType = Session.init
+
+    private var cancellables = Set<AnyCancellable>()
+    private let routeSubject = PassthroughSubject<LaunchRoute, Never>()
+    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
+
+    func viewDidAppear() {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
+            self?.hudSubject.send(.on(nil))
+            self?.checkVersion()
+        }
+    }
+
+    private func checkVersion() {
+        versionChecker().sink { [unowned self] in
+            switch $0 {
+            case .upToDate:
+                versionApproved()
+            case .failure(let error):
+                versionFailed(error: error)
+            case .updateRequired(let info):
+                versionUpdateRequired(info)
+            case .updateRecommended(let info):
+                versionUpdateRecommended(info)
+            }
+        }.store(in: &cancellables)
+    }
+
+    func versionApproved() {
+        network.writeLogs()
+
+        network.updateNDF { [weak self] in
+            guard let self = self else { return }
+
+            switch $0 {
+            case .success(let ndf):
+                self.network.updateErrors()
+
+                guard self.network.hasClient else {
+                    self.hudSubject.send(.none)
+                    self.routeSubject.send(.onboarding(ndf))
+                    self.dropboxService.unlink()
+                    return
+                }
+
+                guard self.username != nil else {
+                    self.network.purgeFiles()
+                    self.hudSubject.send(.none)
+                    self.routeSubject.send(.onboarding(ndf))
+                    self.dropboxService.unlink()
+                    return
+                }
+
+                self.backgroundScheduler.schedule { [weak self] in
+                    guard let self = self else { return }
+
+                    do {
+                        let session = try self.getSession(ndf)
+                        DependencyInjection.Container.shared.register(session as SessionType)
+                        self.hudSubject.send(.none)
+                        self.checkBiometrics()
+                    } catch {
+                        self.hudSubject.send(.error(HUDError(with: error)))
+                    }
+                }
+            case .failure(let error):
+                self.hudSubject.send(.error(HUDError(with: error)))
+            }
+        }
+    }
+
+    func getContactWith(userId: Data) -> Contact? {
+        guard let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
+              let contact = session.getContactWith(userId: userId) else {
+            return nil
+        }
+
+        return contact
+    }
+
+    func getGroupInfoWith(groupId: Data) -> GroupChatInfo? {
+        guard let session: SessionType = try? DependencyInjection.Container.shared.resolve(),
+              let info = session.getGroupChatInfoWith(groupId: groupId) else {
+            return nil
+        }
+
+        return info
+    }
+
+    private func versionFailed(error: Error) {
+        let title = Localized.Launch.Version.failed
+        let content = error.localizedDescription
+        let hudError = HUDError(content: content, title: title, dismissable: false)
+
+        hudSubject.send(.error(hudError))
+    }
+
+    private func versionUpdateRequired(_ info: DappVersionInformation) {
+        hudSubject.send(.none)
+
+        let model = Update(
+            content: info.minimumMessage,
+            urlString: info.appUrl,
+            positiveActionTitle: Localized.Launch.Version.Required.positive,
+            negativeActionTitle: nil,
+            actionStyle: .brandColored
+        )
+
+        routeSubject.send(.update(model))
+    }
+
+    private func versionUpdateRecommended(_ info: DappVersionInformation) {
+        hudSubject.send(.none)
+
+        let model = Update(
+            content: Localized.Launch.Version.Recommended.title,
+            urlString: info.appUrl,
+            positiveActionTitle: Localized.Launch.Version.Recommended.positive,
+            negativeActionTitle: Localized.Launch.Version.Recommended.negative,
+            actionStyle: .simplestColoredRed
+        )
+
+        routeSubject.send(.update(model))
+    }
+
+    private func checkBiometrics() {
+        if permissionHandler.isBiometricsAvailable && isBiometricsOn {
+            permissionHandler.requestBiometrics { [weak self] in
+                switch $0 {
+                case .success(let granted):
+                    guard granted else { return }
+                    self?.routeSubject.send(.chats)
+
+                case .failure(let error):
+                    self?.hudSubject.send(.error(HUDError(with: error)))
+                }
+            }
+        } else {
+            routeSubject.send(.chats)
+        }
+    }
+}
diff --git a/Sources/LaunchFeature/UpdateBlocker.swift b/Sources/LaunchFeature/UpdateBlocker.swift
new file mode 100644
index 00000000..571c2a52
--- /dev/null
+++ b/Sources/LaunchFeature/UpdateBlocker.swift
@@ -0,0 +1,24 @@
+import UIKit
+import Theme
+import Shared
+
+final class UpdateBlocker {
+    private(set) var window: Window? = Window()
+
+    func showWindow() {
+        window?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
+        window?.rootViewController = StatusBarViewController(nil)
+        window?.alpha = 0.0
+        window?.makeKeyAndVisible()
+
+        UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 }
+    }
+
+    func hideWindow() {
+        UIView.animate(withDuration: 0.3) {
+            self.window?.alpha = 0.0
+        } completion: { _ in
+            self.window = nil
+        }
+    }
+}
diff --git a/Sources/Models/Contact.swift b/Sources/Models/Contact.swift
index f80b0ae1..1959743e 100644
--- a/Sources/Models/Contact.swift
+++ b/Sources/Models/Contact.swift
@@ -49,6 +49,7 @@ public struct Contact: Codable, Hashable, Equatable {
         case friends
         case received
         case requested
+        case isRecent
         case verificationInProgress
         case withUserId(Data)
         case withUserIds([Data])
@@ -79,6 +80,7 @@ public struct Contact: Codable, Hashable, Equatable {
     public var createdAt: Date
     public let username: String
     public var nickname: String?
+    public var isRecent: Bool
 
     public init(
         photo: Data?,
@@ -89,7 +91,8 @@ public struct Contact: Codable, Hashable, Equatable {
         marshaled: Data,
         username: String,
         nickname: String?,
-        createdAt: Date
+        createdAt: Date,
+        isRecent: Bool
     ) {
         self.email = email
         self.phone = phone
@@ -100,6 +103,7 @@ public struct Contact: Codable, Hashable, Equatable {
         self.nickname = nickname
         self.marshaled = marshaled
         self.createdAt = createdAt
+        self.isRecent = isRecent
     }
 
     public var differenceIdentifier: Data { userId }
diff --git a/Sources/Models/GenericChatInfo.swift b/Sources/Models/GenericChatInfo.swift
deleted file mode 100644
index e086573e..00000000
--- a/Sources/Models/GenericChatInfo.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-import Foundation
-import DifferenceKit
-
-public struct GenericChatInfo: Codable, Equatable, Hashable {
-    public var contact: Contact?
-    public var groupInfo: GroupChatInfo?
-    public var latestE2EMessage: Message?
-    public var differenceIdentifier: Data { contact?.userId ?? groupInfo!.group.groupId }
-
-    public init(
-        contact: Contact?,
-        groupInfo: GroupChatInfo?,
-        latestE2EMessage: Message?
-    ) {
-        self.contact = contact
-        self.groupInfo = groupInfo
-        self.latestE2EMessage = latestE2EMessage
-    }
-}
-
-extension GenericChatInfo: Differentiable {}
diff --git a/Sources/Models/GroupChatInfo.swift b/Sources/Models/GroupChatInfo.swift
index 073ac9a0..9b39ff6d 100644
--- a/Sources/Models/GroupChatInfo.swift
+++ b/Sources/Models/GroupChatInfo.swift
@@ -3,6 +3,7 @@ import Foundation
 public struct GroupChatInfo: Codable, Equatable, Hashable {
     public enum Request {
         case accepted
+        case fromGroup(Data)
     }
 
     public var group: Group
diff --git a/Sources/NetworkMonitor/MockNetworkMonitor.swift b/Sources/NetworkMonitor/MockNetworkMonitor.swift
index d84355e2..473eb593 100644
--- a/Sources/NetworkMonitor/MockNetworkMonitor.swift
+++ b/Sources/NetworkMonitor/MockNetworkMonitor.swift
@@ -1,4 +1,5 @@
 import Combine
+import Foundation
 
 public struct MockNetworkMonitor: NetworkMonitoring {
     private let statusRelay = PassthroughSubject<NetworkStatus, Never>()
@@ -20,10 +21,24 @@ public struct MockNetworkMonitor: NetworkMonitoring {
     }
 
     public func start() {
-        // TODO
+        simulateOscilation(.available)
     }
 
     public func update(_ status: Bool) {
         // TODO
     }
+
+    private func simulateOscilation(_ status: NetworkStatus) {
+        statusRelay.send(status)
+
+        if status == .available {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                simulateOscilation(.internetNotAvailable)
+            }
+        } else if status == .internetNotAvailable {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                simulateOscilation(.available)
+            }
+        }
+    }
 }
diff --git a/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift b/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift
deleted file mode 100644
index f7e3087e..00000000
--- a/Sources/OnboardingFeature/Controllers/OnboardingLaunchController.swift
+++ /dev/null
@@ -1,165 +0,0 @@
-import HUD
-import UIKit
-import Theme
-import Shared
-import Combine
-import DependencyInjection
-
-public final class OnboardingLaunchController: UIViewController {
-    @Dependency private var hud: HUDType
-    @Dependency private var coordinator: OnboardingCoordinating
-
-    private var imageView = UIImageView()
-    private let blocker = UpdateBlocker()
-    private var cancellables = Set<AnyCancellable>()
-    private let viewModel = OnboardingLaunchViewModel()
-
-    public override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
-
-        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
-            self?.viewModel.didFinishSplash()
-        }
-    }
-
-    public override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-        navigationController?.navigationBar.customize(translucent: true)
-    }
-
-    public override func viewDidLayoutSubviews() {
-        super.viewDidLayoutSubviews()
-
-        let gradient = CAGradientLayer()
-        gradient.colors = [
-            UIColor(red: 122/255, green: 235/255, blue: 239/255, alpha: 1).cgColor,
-            UIColor(red: 56/255, green: 204/255, blue: 232/255, alpha: 1).cgColor,
-            UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor,
-            UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor
-        ]
-
-        gradient.startPoint = CGPoint(x: 1, y: 0)
-        gradient.endPoint = CGPoint(x: 0, y: 1)
-
-        gradient.frame = view.bounds
-        view.layer.insertSublayer(gradient, at: 0)
-    }
-
-    public override func viewDidLoad() {
-        super.viewDidLoad()
-        view.backgroundColor = Asset.neutralWhite.color
-
-        imageView.image = Asset.splash.image
-        imageView.contentMode = .scaleAspectFit
-        view.addSubview(imageView)
-
-        imageView.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-            make.left.equalToSuperview().offset(100)
-        }
-
-        viewModel.hud
-            .receive(on: DispatchQueue.main)
-            .sink { [hud] in hud.update(with: $0) }
-            .store(in: &cancellables)
-
-        viewModel.chatsPublisher
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toChats(from: self) }
-            .store(in: &cancellables)
-
-        viewModel.usernamePublisher
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] in coordinator.toStart(with: $0, from: self) }
-            .store(in: &cancellables)
-
-        viewModel.updatePublisher
-            .receive(on: DispatchQueue.main)
-            .sink { [unowned self] updateModel in
-                let drawerView = UIView()
-                drawerView.backgroundColor = Asset.neutralSecondary.color
-                drawerView.layer.cornerRadius = 5
-
-                let vStack = UIStackView()
-                vStack.axis = .vertical
-                vStack.spacing = 10
-                drawerView.addSubview(vStack)
-
-                vStack.snp.makeConstraints {
-                    $0.top.equalToSuperview().offset(18)
-                    $0.left.equalToSuperview().offset(18)
-                    $0.right.equalToSuperview().offset(-18)
-                    $0.bottom.equalToSuperview().offset(-18)
-                }
-
-                let title = UILabel()
-                title.text = "App Update"
-                title.textAlignment = .center
-                title.textColor = Asset.neutralDark.color
-
-                let body = UILabel()
-                body.numberOfLines = 0
-                body.textAlignment = .center
-                body.textColor = Asset.neutralDark.color
-
-                let update = CapsuleButton()
-                update.publisher(for: .touchUpInside)
-                    .sink { UIApplication.shared.open(.init(string: updateModel.appUrl)!, options: [:]) }
-                    .store(in: &cancellables)
-
-                vStack.addArrangedSubview(title)
-                vStack.addArrangedSubview(body)
-                vStack.addArrangedSubview(update)
-
-                body.text = updateModel.body
-                update.set(
-                    style: updateModel.updateStyle,
-                    title: updateModel.updateTitle
-                )
-
-                if let notNowTitle = updateModel.notNowTitle {
-                    let notNow = CapsuleButton()
-                    notNow.set(style: .simplestColoredRed, title: notNowTitle)
-
-                    notNow.publisher(for: .touchUpInside)
-                        .sink { [unowned self] in
-                            blocker.hideWindow()
-                            viewModel.versionApproved()
-                        }.store(in: &cancellables)
-
-                    vStack.addArrangedSubview(notNow)
-                }
-
-                blocker.window?.addSubview(drawerView)
-                drawerView.snp.makeConstraints {
-                    $0.left.equalToSuperview().offset(18)
-                    $0.center.equalToSuperview()
-                    $0.right.equalToSuperview().offset(-18)
-                }
-
-                blocker.showWindow()
-
-            }.store(in: &cancellables)
-    }
-}
-
-private final class UpdateBlocker {
-    private(set) var window: Window? = Window()
-
-    func showWindow() {
-        window?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
-        window?.rootViewController = StatusBarViewController(nil)
-        window?.alpha = 0.0
-        window?.makeKeyAndVisible()
-
-        UIView.animate(withDuration: 0.3) { self.window?.alpha = 1.0 }
-    }
-
-    func hideWindow() {
-        UIView.animate(withDuration: 0.3) {
-            self.window?.alpha = 0.0
-        } completion: { _ in
-            self.window = nil
-        }
-    }
-}
diff --git a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift
index da4249e3..ca73d7c5 100644
--- a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift
+++ b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift
@@ -11,7 +11,6 @@ public protocol OnboardingCoordinating {
     func toEmail(from: UIViewController)
     func toPhone(from: UIViewController)
     func toWelcome(from: UIViewController)
-    func toStart(with: String, from: UIViewController)
     func toUsername(with: String, from: UIViewController)
     func toRestoreList(with: String, from: UIViewController)
     func toDrawer(_: UIViewController, from: UIViewController)
@@ -45,7 +44,6 @@ public struct OnboardingCoordinator: OnboardingCoordinating {
     var searchFactory: () -> UIViewController
     var welcomeFactory: () -> UIViewController
     var chatListFactory: () -> UIViewController
-    var startFactory: (String) -> UIViewController
     var usernameFactory: (String) -> UIViewController
     var restoreListFactory: (String) -> UIViewController
     var successFactory: (OnboardingSuccessModel) -> UIViewController
@@ -59,7 +57,6 @@ public struct OnboardingCoordinator: OnboardingCoordinating {
         searchFactory: @escaping () -> UIViewController,
         welcomeFactory: @escaping () -> UIViewController,
         chatListFactory: @escaping () -> UIViewController,
-        startFactory: @escaping (String) -> UIViewController,
         usernameFactory: @escaping (String) -> UIViewController,
         restoreListFactory: @escaping (String) -> UIViewController,
         successFactory: @escaping (OnboardingSuccessModel) -> UIViewController,
@@ -69,7 +66,6 @@ public struct OnboardingCoordinator: OnboardingCoordinating {
     ) {
         self.emailFactory = emailFactory
         self.phoneFactory = phoneFactory
-        self.startFactory = startFactory
         self.searchFactory = searchFactory
         self.welcomeFactory = welcomeFactory
         self.successFactory = successFactory
@@ -108,11 +104,6 @@ public extension OnboardingCoordinator {
         replacePresenter.present(screen, from: parent)
     }
 
-    func toStart(with ndf: String, from parent: UIViewController) {
-        let screen = startFactory(ndf)
-        replacePresenter.present(screen, from: parent)
-    }
-
     func toUsername(with ndf: String, from parent: UIViewController) {
         let screen = usernameFactory(ndf)
         replacePresenter.present(screen, from: parent)
diff --git a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift b/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift
deleted file mode 100644
index bfc03a0c..00000000
--- a/Sources/OnboardingFeature/ViewModels/OnboardingLaunchViewModel.swift
+++ /dev/null
@@ -1,152 +0,0 @@
-import HUD
-import Shared
-import Combine
-import Defaults
-import Foundation
-import Integration
-import Permissions
-import VersionChecking
-import CombineSchedulers
-import DependencyInjection
-import DropboxFeature
-
-struct UpdateDrawerModel {
-    let body: String
-    let updateTitle: String
-    let updateStyle: CapsuleButtonStyle
-    let notNowTitle: String?
-    let appUrl: String
-}
-
-final class OnboardingLaunchViewModel {
-    @KeyObject(.username, defaultValue: nil) var username: String?
-    @KeyObject(.biometrics, defaultValue: false) var isBiometricsEnabled: Bool
-
-    @Dependency private var network: XXNetworking
-    @Dependency private var versioning: VersionChecker
-    @Dependency private var permissions: PermissionHandling
-    @Dependency private var dropboxService: DropboxInterface
-
-    var getSession: (String) throws -> SessionType = Session.init
-    var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()
-
-    var chatsPublisher: AnyPublisher<Void, Never> { chatsRelay.eraseToAnyPublisher() }
-    private let chatsRelay = PassthroughSubject<Void, Never>()
-
-    var usernamePublisher: AnyPublisher<String, Never> { usernameRelay.eraseToAnyPublisher() }
-    private let usernameRelay = PassthroughSubject<String, Never>()
-
-    var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
-    private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
-
-    var updatePublisher: AnyPublisher<UpdateDrawerModel, Never> { updateRelay.eraseToAnyPublisher() }
-    private let updateRelay = PassthroughSubject<UpdateDrawerModel, Never>()
-
-    private var cancellables = Set<AnyCancellable>()
-
-    func didFinishSplash() {
-        hudRelay.send(.on(nil))
-
-        versioning()
-            .sink { [unowned self] in
-                switch $0 {
-                case .upToDate:
-                    versionApproved()
-                case .updateRecommended(let info):
-                    hudRelay.send(.none)
-                    updateRelay.send(.init(
-                        body: "There is a new version available that enhance the current performance and usability.",
-                        updateTitle: "Update",
-                        updateStyle: .simplestColoredRed,
-                        notNowTitle: "Not now",
-                        appUrl: info.appUrl
-                    ))
-                case .updateRequired(let info):
-                    hudRelay.send(.none)
-
-                    updateRelay.send(.init(
-                        body: info.minimumMessage,
-                        updateTitle: "Okay",
-                        updateStyle: .brandColored,
-                        notNowTitle: nil,
-                        appUrl: info.appUrl
-                    ))
-                case .failure(let error):
-                    hudRelay.send(.error(.init(
-                        content: error.localizedDescription,
-                        title: "Failed checking app version",
-                        dismissable: false
-                    )))
-                }
-            }
-            .store(in: &cancellables)
-    }
-
-    func versionApproved() {
-        hudRelay.send(.on(nil))
-        network.writeLogs()
-
-        network.updateNDF { [weak self] in
-            switch $0 {
-            case .success(let ndf):
-                self?.ndfApproved(ndf: ndf)
-            case .failure(let error):
-                print(error)
-                self?.hudRelay.send(.error(.init(with: error)))
-            }
-        }
-    }
-
-    private func ndfApproved(ndf: String) {
-        network.updateErrors()
-
-        guard network.hasClient == true else {
-            hudRelay.send(.none)
-            usernameRelay.send(ndf)
-            dropboxService.unlink()
-            return
-        }
-
-        guard username != nil else {
-            network.purgeFiles()
-            hudRelay.send(.none)
-            usernameRelay.send(ndf)
-            dropboxService.unlink()
-            return
-        }
-
-        backgroundScheduler.schedule { [weak self] in
-            guard let self = self else { return }
-
-            do {
-                let session = try self.getSession(ndf)
-                DependencyInjection.Container.shared.register(session as SessionType)
-                self.hudRelay.send(.none)
-                self.checkBiometrics()
-            } catch {
-                self.hudRelay.send(.error(.init(with: error)))
-                print(error.localizedDescription)
-            }
-        }
-    }
-
-    private func checkBiometrics() {
-        if permissions.isBiometricsAvailable && isBiometricsEnabled {
-            permissions.requestBiometrics { result in
-                switch result {
-                case .success(let granted):
-                    if granted {
-                        self.chatsRelay.send()
-                    } else {
-                        // TODO
-                    }
-
-                case .failure(let error):
-                    print(error.localizedDescription)
-                }
-            }
-        } else {
-            self.chatsRelay.send()
-        }
-    }
-}
diff --git a/Sources/PushFeature/ContentsBuilder.swift b/Sources/PushFeature/ContentsBuilder.swift
new file mode 100644
index 00000000..9aa75041
--- /dev/null
+++ b/Sources/PushFeature/ContentsBuilder.swift
@@ -0,0 +1,23 @@
+import UserNotifications
+
+public struct ContentsBuilder {
+    enum Constants {
+        static let threadIdentifier = "new_message_identifier"
+    }
+
+    public var build: (String, Push) -> UNMutableNotificationContent
+}
+
+public extension ContentsBuilder {
+    static let live = ContentsBuilder { title, push in
+        let content = UNMutableNotificationContent()
+        content.badge = 1
+        content.body = title
+        content.title = title
+        content.sound = .default
+        content.userInfo["source"] = push.source
+        content.userInfo["type"] = push.type.rawValue
+        content.threadIdentifier = Constants.threadIdentifier
+        return content
+    }
+}
diff --git a/Sources/PushFeature/MockPushHandler.swift b/Sources/PushFeature/MockPushHandler.swift
new file mode 100644
index 00000000..41aced17
--- /dev/null
+++ b/Sources/PushFeature/MockPushHandler.swift
@@ -0,0 +1,39 @@
+import UIKit
+
+public struct MockPushHandler: PushHandling {
+    public init() {}
+
+    public func registerToken(_ token: Data) {
+        // TODO
+    }
+
+    public func requestAuthorization(
+        _ completion: @escaping (Result<Bool, Error>) -> Void
+    ) {
+        completion(.success(true))
+    }
+
+    public func handlePush(
+        _ notification: [AnyHashable : Any],
+        _ completion: @escaping (UIBackgroundFetchResult) -> Void
+    ) {
+        completion(.noData)
+    }
+
+    public func handlePush(
+        _ request: UNNotificationRequest,
+        _ completion: @escaping (UNNotificationContent) -> Void
+    ) {
+        let content = UNMutableNotificationContent()
+        content.title = String(describing: Self.self)
+        completion(content)
+    }
+
+    public func handleAction(
+        _ router: PushRouter,
+        _ userInfo: [AnyHashable : Any],
+        _ completion: @escaping () -> Void
+    ) {
+        completion()
+    }
+}
diff --git a/Sources/PushFeature/Push.swift b/Sources/PushFeature/Push.swift
new file mode 100644
index 00000000..51c25bd5
--- /dev/null
+++ b/Sources/PushFeature/Push.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+public struct Push {
+    public let type: PushType
+    public let source: Data?
+
+    public init?(type: String, source: Data?) {
+        guard let pushType = PushType(rawValue: type) else {
+            return nil
+        }
+
+        self.type = pushType
+        self.source = source
+    }
+}
diff --git a/Sources/PushFeature/PushExtractor.swift b/Sources/PushFeature/PushExtractor.swift
new file mode 100644
index 00000000..045f0e89
--- /dev/null
+++ b/Sources/PushFeature/PushExtractor.swift
@@ -0,0 +1,38 @@
+import Foundation
+import Integration
+
+public struct PushExtractor {
+    enum Constants {
+        static let preImage = "preImage"
+        static let appGroup = "group.elixxir.messenger"
+        static let notificationData = "notificationData"
+    }
+
+    public var extractFrom: ([AnyHashable: Any]) -> Result<[Push]?, Error>
+}
+
+public extension PushExtractor {
+    static let live = PushExtractor { dictionary in
+        var error: NSError?
+
+        guard let data = dictionary[Constants.notificationData] as? String,
+              let defaults = UserDefaults(suiteName: Constants.appGroup),
+              let preImage = defaults.value(forKey: Constants.preImage) as? String,
+              let reports = evaluateNotification(data, preImage, &error) else {
+            return .success(nil)
+        }
+
+        if let error = error {
+            return .failure(error)
+        }
+
+        let pushes = (0..<reports.len())
+            .compactMap { try? reports.get(index: $0) }
+            .filter { $0.forMe() }
+            .filter { $0.type() != PushType.silent.rawValue }
+            .filter { $0.type() != PushType.default.rawValue }
+            .compactMap { Push(type: $0.type(), source: $0.source()) }
+
+        return .success(pushes)
+    }
+}
diff --git a/Sources/PushFeature/PushHandler.swift b/Sources/PushFeature/PushHandler.swift
new file mode 100644
index 00000000..bb0b19cd
--- /dev/null
+++ b/Sources/PushFeature/PushHandler.swift
@@ -0,0 +1,165 @@
+import UIKit
+import Models
+import Defaults
+import Database
+import Integration
+import DependencyInjection
+
+public final class PushHandler: PushHandling {
+    private enum Constants {
+        static let appGroup = "group.elixxir.messenger"
+        static let usernamesSetting = "isShowingUsernames"
+    }
+
+    @KeyObject(.pushNotifications, defaultValue: false) var isPushEnabled: Bool
+
+    let requestAuth: RequestAuth
+    public static let defaultRequestAuth = UNUserNotificationCenter.current().requestAuthorization
+    public typealias RequestAuth = (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void
+
+    public var pushExtractor: PushExtractor
+    public var contentsBuilder: ContentsBuilder
+    public var applicationState: () -> UIApplication.State
+
+    public init(
+        requestAuth: @escaping RequestAuth = defaultRequestAuth,
+        pushExtractor: PushExtractor = .live,
+        contentsBuilder: ContentsBuilder = .live,
+        applicationState: @escaping () -> UIApplication.State = { UIApplication.shared.applicationState }
+    ) {
+        self.requestAuth = requestAuth
+        self.pushExtractor = pushExtractor
+        self.contentsBuilder = contentsBuilder
+        self.applicationState = applicationState
+    }
+
+    public func registerToken(_ token: Data) {
+        do {
+            let session = try DependencyInjection.Container.shared.resolve() as SessionType
+            try session.registerNotifications(token)
+        } catch {
+            isPushEnabled = false
+        }
+    }
+
+    public func requestAuthorization(
+        _ completion: @escaping (Result<Bool, Error>) -> Void
+    ) {
+        let options: UNAuthorizationOptions = [.alert, .sound, .badge]
+
+        requestAuth(options) { granted, error in
+            guard let error = error else {
+                completion(.success(granted))
+                return
+            }
+
+            completion(.failure(error))
+        }
+    }
+
+    public func handlePush(
+        _ userInfo: [AnyHashable: Any],
+        _ completion: @escaping (UIBackgroundFetchResult) -> Void
+    ) {
+        do {
+            guard
+                let pushes = try pushExtractor.extractFrom(userInfo).get(),
+                applicationState() == .background,
+                pushes.isEmpty == false
+            else {
+                completion(.noData)
+                return
+            }
+
+            let content = contentsBuilder.build("New Messages Available", pushes.first!)
+            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+            let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger)
+
+            UNUserNotificationCenter.current().add(request) { error in
+                if error == nil {
+                    completion(.newData)
+                } else {
+                    completion(.failed)
+                }
+            }
+        } catch {
+            completion(.failed)
+        }
+    }
+
+    public func handlePush(
+        _ request: UNNotificationRequest,
+        _ completion: @escaping (UNNotificationContent) -> Void
+    ) {
+        guard let pushes = try? pushExtractor.extractFrom(request.content.userInfo).get(), !pushes.isEmpty,
+              let defaults = UserDefaults(suiteName: Constants.appGroup) else {
+            return
+        }
+
+        guard let showSender = defaults.value(forKey: Constants.usernamesSetting) as? Bool, showSender == true else {
+            pushes.map { ($0.type.unknownSenderContent!, $0) }
+                .map(contentsBuilder.build)
+                .forEach { completion($0) }
+            return
+        }
+
+        let dbManager = GRDBDatabaseManager()
+        try? dbManager.setup()
+
+        let tuples: [(String, Push)] = pushes.compactMap {
+            guard let userId = $0.source, let contact: Contact = try? dbManager.fetch(.withUserId(userId)).first else {
+                return ($0.type.unknownSenderContent!, $0)
+            }
+
+            let name = contact.nickname ?? contact.username
+            return ($0.type.knownSenderContent(name)!, $0)
+        }
+
+        tuples
+            .map(contentsBuilder.build)
+            .forEach { completion($0) }
+    }
+
+    public func handleAction(
+        _ router: PushRouter,
+        _ userInfo: [AnyHashable : Any],
+        _ completion: @escaping () -> Void
+    ) {
+        guard let typeString = userInfo["type"] as? String,
+              let type = PushType(rawValue: typeString) else {
+            completion()
+            return
+        }
+
+        let route: PushRouter.Route
+
+        switch type {
+        case .e2e:
+            guard let source = userInfo["source"] as? Data else {
+                completion()
+                return
+            }
+
+            route = .contactChat(id: source)
+
+        case .group:
+            guard let source = userInfo["source"] as? Data else {
+                completion()
+                return
+            }
+
+            route = .groupChat(id: source)
+
+        case .request, .groupRq:
+            route = .requests
+
+        case .silent, .`default`:
+            fatalError("Silent/Default push types should be filtered at this point")
+
+        case .reset, .endFT, .confirm:
+            route = .requests
+        }
+
+        router.navigateTo(route, completion)
+    }
+}
diff --git a/Sources/PushFeature/PushHandling.swift b/Sources/PushFeature/PushHandling.swift
new file mode 100644
index 00000000..c17c7e60
--- /dev/null
+++ b/Sources/PushFeature/PushHandling.swift
@@ -0,0 +1,70 @@
+import UIKit
+
+public protocol PushHandling {
+
+    /// Submits the APNS token to a 3rd-party service.
+    /// This should be called whenever the user accepts
+    /// receiving remote push notifications.
+    ///
+    /// - Parameters:
+    ///   - token: The APNS provided token
+    ///
+    func registerToken(
+        _ token: Data
+    )
+
+    /// Prompts a system alert to the user requesting
+    /// permission for receiving remote push notifications
+    ///
+    /// - Parameters:
+    ///   - completion: Async result closure containing the user reponse
+    ///
+    func requestAuthorization(
+        _ completion: @escaping (Result<Bool, Error>) -> Void
+    )
+
+    /// Evaluates if the notification should be displayed or not
+    /// and if yes, how should it look like.
+    ///
+    /// - Note: This function should be called by the main app target
+    /// - Warning: The notifications should only appear if the app is in background
+    ///
+    /// - Parameters:
+    ///   - userInfo: Dictionary contaning the payload of the remote push
+    ///   - completion: Async closure containing the operation chosed
+    ///
+    func handlePush(
+        _ userInfo: [AnyHashable: Any],
+        _ completion: @escaping (UIBackgroundFetchResult) -> Void
+    )
+
+    /// Evaluates if the notification should be displayed or not
+    ///  and if yes, how it should look like and who is it from
+    ///
+    /// - Note: This function should be called by the `NotificationExtension`
+    ///
+    /// - Parameters:
+    ///   - request: The notification request that arrived for the `NotificationExtension`
+    ///   - completion: Async closure containing the operation chosed
+    ///
+    func handlePush(
+        _ request: UNNotificationRequest,
+        _ completion: @escaping (UNNotificationContent) -> Void
+    )
+
+    /// Deeplinks to any UI flow set within the notification.
+    /// It can get called either when the user starts the app
+    /// from a notification or when the user has the app in
+    /// background and resumes the app by tapping on a push
+    ///
+    /// - Parameters:
+    ///   - router: Router instance that will decide the correct UI flow
+    ///   - userInfo: Dictionary contaning the payload of the notification
+    ///   - completion: Async empty closure
+    ///
+    func handleAction(
+        _ router: PushRouter,
+        _ userInfo: [AnyHashable: Any],
+        _ completion: @escaping () -> Void
+    )
+}
diff --git a/Sources/PushFeature/PushRouter.swift b/Sources/PushFeature/PushRouter.swift
new file mode 100644
index 00000000..6fc66797
--- /dev/null
+++ b/Sources/PushFeature/PushRouter.swift
@@ -0,0 +1,22 @@
+import Foundation
+
+public struct PushRouter {
+    public typealias NavigateTo = (Route, @escaping () -> Void) -> Void
+
+    public enum Route {
+        case requests
+        case groupChat(id: Data)
+        case contactChat(id: Data)
+    }
+
+    public var navigateTo: NavigateTo
+
+    public init(navigateTo: @escaping NavigateTo) {
+        self.navigateTo = navigateTo
+    }
+}
+
+public extension PushRouter {
+    static let noop = PushRouter { _, _ in }
+}
+
diff --git a/Sources/PushFeature/PushType.swift b/Sources/PushFeature/PushType.swift
new file mode 100644
index 00000000..7cd2fefe
--- /dev/null
+++ b/Sources/PushFeature/PushType.swift
@@ -0,0 +1,53 @@
+public enum PushType: String {
+    case e2e
+    case reset
+    case endFT
+    case group
+    case silent
+    case groupRq
+    case confirm
+    case request
+    case `default`
+
+    var unknownSenderContent: String? {
+        switch self {
+        case .silent, .`default`:
+            return nil
+        case .endFT:
+            return "New media received"
+        case .group:
+            return "New group message"
+        case .groupRq:
+            return "Group request received"
+        case .e2e:
+            return "New private message"
+        case .reset:
+            return "One of your contacts has restored their account"
+        case .request:
+            return "Request received"
+        case .confirm:
+            return "Request accepted"
+        }
+    }
+
+    var knownSenderContent: (String) -> String? {
+        switch self {
+        case .silent, .`default`:
+            return { _ in nil }
+        case .e2e:
+            return { String(format: "%@ sent you a private message", $0) }
+        case .reset:
+            return { String(format: "%@ restored their account", $0) }
+        case .endFT:
+            return { String(format: "%@ sent you a file", $0) }
+        case .group:
+            return { String(format: "%@ sent you a group message", $0) }
+        case .groupRq:
+            return { String(format: "%@ sent you a group request", $0) }
+        case .confirm:
+            return { String(format: "%@ confirmed your contact request", $0) }
+        case .request:
+            return { String(format: "%@ sent you a contact request", $0) }
+        }
+    }
+}
diff --git a/Sources/PushNotifications/MockPushHandler.swift b/Sources/PushNotifications/MockPushHandler.swift
deleted file mode 100644
index d49dba16..00000000
--- a/Sources/PushNotifications/MockPushHandler.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-import UIKit
-
-public struct MockPushHandler: PushHandling {
-    public init() {}
-
-    public func didRegisterWith(_ deviceToken: Data) {}
-
-    public func didRequestAuthorization(
-        _ completion: @escaping (Result<Bool, Error>) -> Void
-    ) {}
-
-    public func didReceiveRemote(
-        _ notification: [AnyHashable : Any],
-        _ completion: @escaping (UIBackgroundFetchResult) -> Void
-    ) {}
-}
diff --git a/Sources/PushNotifications/PushHandler.swift b/Sources/PushNotifications/PushHandler.swift
deleted file mode 100644
index 4a2c09d8..00000000
--- a/Sources/PushNotifications/PushHandler.swift
+++ /dev/null
@@ -1,102 +0,0 @@
-import UIKit
-import XXLogger
-import Defaults
-import os.log
-import Integration
-import UserNotifications
-import DependencyInjection
-
-public protocol PushHandling {
-    func didRegisterWith(_ deviceToken: Data)
-
-    func didRequestAuthorization(_ completion: @escaping (Result<Bool, Error>) -> Void)
-
-    func didReceiveRemote(_ notification: [AnyHashable : Any],
-                          _ completion: @escaping (UIBackgroundFetchResult) -> Void)
-}
-
-public final class PushHandler: PushHandling {
-    @Dependency private var logger: XXLogger
-
-    @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications: Bool
-
-    public init() {}
-
-    public func didReceiveRemote(
-        _ notification: [AnyHashable : Any],
-        _ completion: @escaping (UIBackgroundFetchResult) -> Void
-    ) {
-        guard let data = notification["notificationData"] as? String,
-              let defaults = UserDefaults(suiteName: "group.io.xxlabs.notification"),
-              let preImage = defaults.value(forKey: "preImage") as? String else {
-                  completion(.newData)
-                  return
-              }
-
-        var error: NSError?
-
-        guard let reports = evaluateNotification(data, preImage, &error) else { return }
-        let length = reports.len()
-
-        var showNotification = false
-
-        for index in 0..<length {
-            if let report = try? reports.get(index: index) {
-                let isForMe = report.forMe()
-                let isNotDefault = report.type() != "default"
-                let isNotSilent = report.type() != "silent"
-
-                if isForMe && isNotSilent && isNotDefault {
-                    showNotification = true
-                    break
-                }
-            }
-        }
-
-        guard showNotification == true else { return }
-
-        guard error == nil else {
-            logger.error(error as Any)
-            return
-        }
-
-        let content = UNMutableNotificationContent()
-        content.title = "New Messages Available"
-        content.sound = UNNotificationSound.default
-
-        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
-        let request = UNNotificationRequest(identifier: "io.xxlabs.messenger", content: content, trigger: trigger)
-
-        UNUserNotificationCenter.current().add(request) { [weak self] in self?.logger.error($0 as Any) }
-    }
-
-    public func didRequestAuthorization(_ completion: @escaping (Result<Bool, Error>) -> Void) {
-        let center = UNUserNotificationCenter.current()
-        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
-            if let error = error {
-                completion(.failure(error))
-                return
-            }
-
-            completion(.success(granted))
-        }
-    }
-
-    public func didRegisterWith(_ deviceToken: Data) {
-        do {
-            logger.info("PushHandling: didRegisterWith(_ deviceToken: Data)")
-            let session = try DependencyInjection.Container.shared.resolve() as SessionType
-            try session.registerNotifications(deviceToken.hexEncodedString)
-            logger.info("PushHandling: didRegisterWith(_ deviceToken: Data) success")
-        } catch {
-            pushNotifications = false
-            logger.error(error)
-        }
-    }
-}
-
-private extension Data {
-    var hexEncodedString: String {
-        map { String(format: "%02hhx", $0) }.joined()
-    }
-}
diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift
index a77cc3a3..b88932c1 100644
--- a/Sources/RestoreFeature/Controllers/RestoreController.swift
+++ b/Sources/RestoreFeature/Controllers/RestoreController.swift
@@ -36,7 +36,7 @@ public final class RestoreController: UIViewController {
         navigationItem.backButtonTitle = ""
 
         let title = UILabel()
-        title.text = Localized.Restore.header
+        title.text = Localized.AccountRestore.header
         title.textColor = Asset.neutralActive.color
         title.font = Fonts.Mulish.semiBold.font(size: 18.0)
 
@@ -95,20 +95,20 @@ public final class RestoreController: UIViewController {
 extension RestoreController {
     private func presentWarning() {
         let actionButton = DrawerCapsuleButton(model: .init(
-            title: Localized.Restore.Warning.action,
+            title: Localized.AccountRestore.Warning.action,
             style: .brandColored
         ))
 
         let drawer = DrawerController(with: [
             DrawerText(
                 font: Fonts.Mulish.bold.font(size: 26.0),
-                text: Localized.Restore.Warning.title,
+                text: Localized.AccountRestore.Warning.title,
                 color: Asset.neutralActive.color,
                 alignment: .left,
                 spacingAfter: 19
             ),
             DrawerText(
-                text: Localized.Restore.Warning.subtitle,
+                text: Localized.AccountRestore.Warning.subtitle,
                 spacingAfter: 37
             ),
             actionButton
diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift
index 43767aed..041717b6 100644
--- a/Sources/RestoreFeature/Controllers/RestoreListController.swift
+++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift
@@ -90,18 +90,18 @@ public final class RestoreListController: UIViewController {
 extension RestoreListController {
     private func presentWarning() {
         let actionButton = DrawerCapsuleButton(model: .init(
-            title: Localized.Restore.Warning.action,
+            title: Localized.AccountRestore.Warning.action,
             style: .brandColored
         ))
 
         let drawer = DrawerController(with: [
             DrawerText(
                 font: Fonts.Mulish.bold.font(size: 26.0),
-                text: Localized.Restore.Warning.title,
+                text: Localized.AccountRestore.Warning.title,
                 spacingAfter: 19
             ),
             DrawerText(
-                text: Localized.Restore.Warning.subtitle,
+                text: Localized.AccountRestore.Warning.subtitle,
                 spacingAfter: 37
             ),
             actionButton
diff --git a/Sources/RestoreFeature/Views/RestoreListView.swift b/Sources/RestoreFeature/Views/RestoreListView.swift
index 26d937db..2a688760 100644
--- a/Sources/RestoreFeature/Views/RestoreListView.swift
+++ b/Sources/RestoreFeature/Views/RestoreListView.swift
@@ -15,15 +15,15 @@ final class RestoreListView: UIView {
         super.init(frame: .zero)
         backgroundColor = Asset.neutralWhite.color
 
-        setupTitle(Localized.Restore.List.title)
-        setupSubtitle(Localized.Restore.List.firstSubtitle)
+        setupTitle(Localized.AccountRestore.List.title)
+        setupSubtitle(Localized.AccountRestore.List.firstSubtitle)
 
         let paragraph = NSMutableParagraphStyle()
         paragraph.alignment = .left
         paragraph.lineHeightMultiple = 1.15
 
         let attrString = NSMutableAttributedString(
-            string: Localized.Restore.List.secondSubtitle,
+            string: Localized.AccountRestore.List.secondSubtitle,
             attributes: [
                 .foregroundColor: Asset.neutralBody.color,
                 .font: Fonts.Mulish.regular.font(size: 16.0) as Any,
@@ -38,7 +38,7 @@ final class RestoreListView: UIView {
         dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image)
         driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image)
 
-        cancelButton.set(style: .seeThrough, title: Localized.Restore.List.cancel)
+        cancelButton.set(style: .seeThrough, title: Localized.AccountRestore.List.cancel)
 
         stackView.axis = .vertical
         stackView.addArrangedSubview(driveButton)
diff --git a/Sources/RestoreFeature/Views/RestoreSuccessView.swift b/Sources/RestoreFeature/Views/RestoreSuccessView.swift
index 94f31895..35bdd4c0 100644
--- a/Sources/RestoreFeature/Views/RestoreSuccessView.swift
+++ b/Sources/RestoreFeature/Views/RestoreSuccessView.swift
@@ -46,8 +46,8 @@ final class RestoreSuccessView: UIView {
             make.bottom.equalToSuperview().offset(-60)
         }
 
-        setTitle(Localized.Restore.Success.title)
-        setSubtitle(Localized.Restore.Success.subtitle)
+        setTitle(Localized.AccountRestore.Success.title)
+        setSubtitle(Localized.AccountRestore.Success.subtitle)
     }
 
     required init?(coder: NSCoder) { nil }
diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift
index 52686480..e36e5d5b 100644
--- a/Sources/RestoreFeature/Views/RestoreView.swift
+++ b/Sources/RestoreFeature/Views/RestoreView.swift
@@ -23,9 +23,9 @@ final class RestoreView: UIView {
         titleLabel.textColor = Asset.neutralDark.color
         subtitleLabel.textColor = Asset.neutralDark.color
 
-        restoreButton.set(style: .brandColored, title: Localized.Restore.Found.restore)
-        cancelButton.set(style: .simplestColoredBrand, title: Localized.Restore.Found.cancel)
-        backButton.set(style: .seeThrough, title: Localized.Restore.NotFound.back)
+        restoreButton.set(style: .brandColored, title: Localized.AccountRestore.Found.restore)
+        cancelButton.set(style: .simplestColoredBrand, title: Localized.AccountRestore.Found.cancel)
+        backButton.set(style: .seeThrough, title: Localized.AccountRestore.NotFound.back)
 
         bottomStackView.axis = .vertical
 
@@ -105,20 +105,20 @@ final class RestoreView: UIView {
     }
 
     private func showBackup(_ backup: Backup, fromCloud cloud: CloudService) {
-        titleLabel.text = Localized.Restore.Found.title
-        subtitleLabel.text = Localized.Restore.Found.subtitle
+        titleLabel.text = Localized.AccountRestore.Found.title
+        subtitleLabel.text = Localized.AccountRestore.Found.subtitle
 
         detailsView.titleLabel.text = cloud.name()
         detailsView.imageView.image = cloud.asset()
 
         detailsView.dateView.setup(
-            title: Localized.Restore.Found.date,
+            title: Localized.AccountRestore.Found.date,
             value: backup.date.backupStyle(),
             hasArrow: false
         )
 
         detailsView.sizeView.setup(
-            title: Localized.Restore.Found.size,
+            title: Localized.AccountRestore.Found.size,
             value: String(format: "%.1f kb", backup.size/1000),
             hasArrow: false
         )
@@ -131,8 +131,8 @@ final class RestoreView: UIView {
     }
 
     private func showNoBackupForCloud(named cloud: String) {
-        titleLabel.text = Localized.Restore.NotFound.title
-        subtitleLabel.text = Localized.Restore.NotFound.subtitle(cloud)
+        titleLabel.text = Localized.AccountRestore.NotFound.title
+        subtitleLabel.text = Localized.AccountRestore.NotFound.subtitle(cloud)
 
         restoreButton.isHidden = true
         cancelButton.isHidden = true
diff --git a/Sources/ScanFeature/Controllers/ScanContainerController.swift b/Sources/ScanFeature/Controllers/ScanContainerController.swift
index f02b4ef8..fb1f9d44 100644
--- a/Sources/ScanFeature/Controllers/ScanContainerController.swift
+++ b/Sources/ScanFeature/Controllers/ScanContainerController.swift
@@ -1,8 +1,8 @@
 import UIKit
-import DrawerFeature
 import Theme
 import Shared
 import Combine
+import DrawerFeature
 import DependencyInjection
 
 public final class ScanContainerController: UIViewController {
@@ -11,25 +11,59 @@ public final class ScanContainerController: UIViewController {
 
     lazy private var screenView = ScanContainerView()
 
+    private var previousPoint: CGPoint = .zero
+    private let scanController = ScanController()
+    private let displayController = ScanDisplayController()
+
     private var cancellables = Set<AnyCancellable>()
     private var drawerCancellables = Set<AnyCancellable>()
 
     public override func loadView() {
         view = screenView
-
         screenView.scrollView.delegate = self
-        addChild(screenView.scanScreen)
-        screenView.scanScreen.didMove(toParent: self)
 
-        addChild(screenView.displayScreen)
-        screenView.displayScreen.didMove(toParent: self)
+        addChild(scanController)
+        addChild(displayController)
+
+        screenView.scrollView.addSubview(scanController.view)
+        screenView.scrollView.addSubview(displayController.view)
+
+        scanController.view.snp.makeConstraints {
+            $0.top.equalTo(screenView)
+            $0.width.equalTo(screenView)
+            $0.bottom.equalTo(screenView)
+            $0.left.equalToSuperview()
+            $0.right.equalTo(displayController.view.snp.left)
+        }
+
+        displayController.view.snp.makeConstraints {
+            $0.top.equalTo(screenView.segmentedControl.snp.bottom)
+            $0.width.equalTo(scanController.view)
+            $0.bottom.equalTo(scanController.view)
+        }
+
+        scanController.didMove(toParent: self)
+        displayController.didMove(toParent: self)
+
         screenView.bringSubviewToFront(screenView.segmentedControl)
     }
 
+    public override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+        screenView.scrollView.contentOffset = previousPoint
+    }
+
+    public override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        previousPoint = screenView.scrollView.contentOffset
+        screenView.scrollView.contentOffset = .zero
+    }
+
     public override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         statusBarController.style.send(.lightContent)
         navigationController?.navigationBar.customize(translucent: true)
+        screenView.scrollView.contentOffset = .zero
     }
 
     public override func viewDidLoad() {
@@ -37,12 +71,22 @@ public final class ScanContainerController: UIViewController {
         setupNavigationBar()
         setupBindings()
 
-        screenView.displayScreen.didTapInfo = { [weak self] in
+        displayController.didTapInfo = { [weak self] in
             self?.presentInfo(
                 title: Localized.Scan.Info.title,
                 subtitle: Localized.Scan.Info.subtitle
             )
         }
+
+        displayController.didTapAddEmail = { [weak self] in
+            guard let self = self else { return }
+            self.coordinator.toEmail(from: self)
+        }
+
+        displayController.didTapAddPhone = { [weak self] in
+            guard let self = self else { return }
+            self.coordinator.toPhone(from: self)
+        }
     }
 
     private func setupNavigationBar() {
@@ -85,8 +129,8 @@ public final class ScanContainerController: UIViewController {
     public func scrollViewDidScroll(_ scrollView: UIScrollView) {
         let percentage = scrollView.contentOffset.x / view.frame.width
 
-        screenView.scanScreen.view.alpha = 1 - percentage
-        screenView.displayScreen.view.alpha = percentage
+        scanController.view.alpha = 1 - percentage
+        displayController.view.alpha = percentage
         screenView.segmentedControl.updateLeftConstraint(percentage)
     }
 
diff --git a/Sources/ScanFeature/Controllers/ScanController.swift b/Sources/ScanFeature/Controllers/ScanController.swift
index c172703b..2a1b99aa 100644
--- a/Sources/ScanFeature/Controllers/ScanController.swift
+++ b/Sources/ScanFeature/Controllers/ScanController.swift
@@ -90,10 +90,6 @@ final class ScanController: UIViewController {
             .sink { [unowned self] in
                 status = $0
                 screenView.update(with: $0)
-
-                if case .failed(_) = $0 {
-                    camera.stop()
-                }
             }.store(in: &cancellables)
 
         screenView.actionButton.publisher(for: .touchUpInside)
diff --git a/Sources/ScanFeature/Controllers/ScanDisplayController.swift b/Sources/ScanFeature/Controllers/ScanDisplayController.swift
index f772340f..e373d4de 100644
--- a/Sources/ScanFeature/Controllers/ScanDisplayController.swift
+++ b/Sources/ScanFeature/Controllers/ScanDisplayController.swift
@@ -1,6 +1,5 @@
 import UIKit
 import Combine
-import Countries
 
 final class ScanDisplayController: UIViewController {
     lazy private var screenView = ScanDisplayView()
@@ -9,51 +8,57 @@ final class ScanDisplayController: UIViewController {
     private var cancellables = Set<AnyCancellable>()
 
     var didTapInfo: (() -> Void)?
+    var didTapAddPhone: (() -> Void)?
+    var didTapAddEmail: (() -> Void)?
 
     override func loadView() {
         view = screenView
     }
 
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        viewModel.loadCached()
+        viewModel.generateQR()
+    }
+
     override func viewDidLoad() {
         super.viewDidLoad()
 
-        viewModel.state
-            .map(\.image)
+        viewModel.statePublisher
             .receive(on: DispatchQueue.main)
             .sink { [unowned self] in
-                guard let ciimage = $0 else { return }
-                screenView.qrImage.image = UIImage(ciImage: ciimage)
+                if let image = $0.image {
+                    screenView.setup(code: image)
+                }
+
+                screenView.setupAttributes(
+                    email: $0.email,
+                    phone: $0.phone,
+                    emailSharing: $0.isSharingEmail,
+                    phoneSharing: $0.isSharingPhone
+                )
             }.store(in: &cancellables)
 
-        if viewModel.email != nil || viewModel.phone != nil {
-            screenView.setupShareView { [weak self] in self?.didTapInfo?() }
-
-            if let email = viewModel.email {
-                screenView.shareView.setup(email: email)
-                    .sink { [unowned self] in viewModel.didToggleEmail() }
-                    .store(in: &cancellables)
-
-                viewModel.state.map(\.isSharingEmail)
-                    .removeDuplicates()
-                    .receive(on: DispatchQueue.main)
-                    .sink { [unowned self] in screenView.shareView.emailView.switcherView.setOn($0, animated: false) }
-                    .store(in: &cancellables)
-            }
-
-            if let phone = viewModel.phone {
-                let fullPhone = "\(Country.findFrom(phone).prefix)\(phone.dropLast(2))"
-
-                screenView.shareView.setup(phone: fullPhone)
-                    .sink { [unowned self] in viewModel.didTogglePhone() }
-                    .store(in: &cancellables)
-
-                viewModel.state.map(\.isSharingPhone)
-                    .removeDuplicates()
-                    .receive(on: DispatchQueue.main)
-                    .sink { [unowned self] in screenView.shareView.phoneView.switcherView.setOn($0, animated: false) }
-                    .store(in: &cancellables)
-            }
-        }
+        screenView.actionPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [unowned self] in
+                switch $0 {
+                case .info:
+                    didTapInfo?()
+
+                case .addEmail:
+                    didTapAddEmail?()
+
+                case .addPhone:
+                    didTapAddPhone?()
+
+                case .toggleEmail:
+                    viewModel.didToggleEmail()
+
+                case .togglePhone:
+                    viewModel.didTogglePhone()
+                }
+            }.store(in: &cancellables)
 
         viewModel.loadCached()
         viewModel.generateQR()
diff --git a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift
index 86ed4bc1..6a36db18 100644
--- a/Sources/ScanFeature/Coordinator/ScanCoordinator.swift
+++ b/Sources/ScanFeature/Coordinator/ScanCoordinator.swift
@@ -5,6 +5,8 @@ import Presentation
 import ContactFeature
 
 public protocol ScanCoordinating {
+    func toEmail(from: UIViewController)
+    func toPhone(from: UIViewController)
     func toContacts(from: UIViewController)
     func toRequests(from: UIViewController)
     func toSideMenu(from: UIViewController)
@@ -18,17 +20,23 @@ public struct ScanCoordinator: ScanCoordinating {
     var bottomPresenter: Presenting = BottomPresenter()
     var replacePresenter: Presenting = ReplacePresenter(mode: .replaceLast)
 
+    var emailFactory: () -> UIViewController
+    var phoneFactory: () -> UIViewController
     var contactsFactory: () -> UIViewController
     var requestsFactory: () -> UIViewController
     var contactFactory: (Contact) -> UIViewController
     var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController
 
     public init(
+        emailFactory: @escaping () -> UIViewController,
+        phoneFactory: @escaping () -> UIViewController,
         contactsFactory: @escaping () -> UIViewController,
         requestsFactory: @escaping () -> UIViewController,
         contactFactory: @escaping (Contact) -> UIViewController,
         sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController
     ) {
+        self.emailFactory = emailFactory
+        self.phoneFactory = phoneFactory
         self.contactFactory = contactFactory
         self.contactsFactory = contactsFactory
         self.requestsFactory = requestsFactory
@@ -37,6 +45,21 @@ public struct ScanCoordinator: ScanCoordinating {
 }
 
 public extension ScanCoordinator {
+    func toContact(
+        _ contact: Contact,
+        from parent: UIViewController
+    ) {
+        let screen = contactFactory(contact)
+        pushPresenter.present(screen, from: parent)
+    }
+
+    func toDrawer(
+        _ drawer: UIViewController,
+        from parent: UIViewController
+    ) {
+        bottomPresenter.present(drawer, from: parent)
+    }
+
     func toRequests(from parent: UIViewController) {
         let screen = requestsFactory()
         replacePresenter.present(screen, from: parent)
@@ -47,17 +70,18 @@ public extension ScanCoordinator {
         replacePresenter.present(screen, from: parent)
     }
 
-    func toContact(_ contact: Contact, from parent: UIViewController) {
-        let screen = contactFactory(contact)
-        pushPresenter.present(screen, from: parent)
+    func toSideMenu(from parent: UIViewController) {
+        let screen = sideMenuFactory(.scan, parent)
+        sidePresenter.present(screen, from: parent)
     }
 
-    public func toDrawer(_ drawer: UIViewController, from parent: UIViewController) {
-        bottomPresenter.present(drawer, from: parent)
+    func toEmail(from parent: UIViewController) {
+        let screen = emailFactory()
+        pushPresenter.present(screen, from: parent)
     }
 
-    func toSideMenu(from parent: UIViewController) {
-        let screen = sideMenuFactory(.scan, parent)
-        sidePresenter.present(screen, from: parent)
+    func toPhone(from parent: UIViewController) {
+        let screen = phoneFactory()
+        pushPresenter.present(screen, from: parent)
     }
 }
diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
index 00ad9e1e..df456065 100644
--- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
+++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift
@@ -1,46 +1,57 @@
 import UIKit
 import Combine
 import Defaults
+import Countries
 import Integration
 import DependencyInjection
 
 struct ScanDisplayViewState: Equatable {
     var image: CIImage?
+    var email: String?
+    var phone: String?
     var isSharingEmail: Bool = false
     var isSharingPhone: Bool = false
 }
 
 final class ScanDisplayViewModel {
-    // MARK: Stored
-
-    @KeyObject(.email, defaultValue: nil) var email: String?
-    @KeyObject(.phone, defaultValue: nil) var phone: String?
-    @KeyObject(.sharingEmail, defaultValue: false) var sharingEmail: Bool
-    @KeyObject(.sharingPhone, defaultValue: false) var sharingPhone: Bool
+    @Dependency private var session: SessionType
 
-    // MARK: Properties
+    @KeyObject(.email, defaultValue: nil) private var email: String?
+    @KeyObject(.phone, defaultValue: nil) private var phone: String?
+    @KeyObject(.sharingEmail, defaultValue: false) private var sharingEmail: Bool
+    @KeyObject(.sharingPhone, defaultValue: false) private var sharingPhone: Bool
 
-    var state: AnyPublisher<ScanDisplayViewState, Never> { stateRelay.eraseToAnyPublisher() }
-    private let stateRelay = CurrentValueSubject<ScanDisplayViewState, Never>(.init())
+    var statePublisher: AnyPublisher<ScanDisplayViewState, Never> {
+        stateSubject.eraseToAnyPublisher()
+    }
 
-    // MARK: Injected
+    private let stateSubject = CurrentValueSubject<ScanDisplayViewState, Never>(.init())
 
-    @Dependency private var session: SessionType
+    func loadCached() {
+        var cleanPhone: String?
 
-    // MARK: Public
+        if let dirtyPhone = phone {
+            cleanPhone = "\(Country.findFrom(dirtyPhone).prefix)\(dirtyPhone.dropLast(2))"
+        }
 
-    func loadCached() {
-        stateRelay.value.isSharingEmail = sharingEmail
-        stateRelay.value.isSharingPhone = sharingPhone
+        stateSubject.value = .init(
+            image: stateSubject.value.image,
+            email: email,
+            phone: cleanPhone,
+            isSharingEmail: sharingEmail,
+            isSharingPhone: sharingPhone
+        )
     }
 
     func didToggleEmail() {
         sharingEmail.toggle()
+        stateSubject.value.isSharingEmail = sharingEmail
         generateQR()
     }
 
     func didTogglePhone() {
         sharingPhone.toggle()
+        stateSubject.value.isSharingPhone = sharingPhone
         generateQR()
     }
 
@@ -51,7 +62,7 @@ final class ScanDisplayViewModel {
         let transform = CGAffineTransform(scaleX: 5, y: 5)
 
         if let output = filter.outputImage?.transformed(by: transform) {
-            stateRelay.value.image = output
+            stateSubject.value.image = output
         }
     }
 }
diff --git a/Sources/ScanFeature/ViewModels/ScanViewModel.swift b/Sources/ScanFeature/ViewModels/ScanViewModel.swift
index 8656ad14..c3f51841 100644
--- a/Sources/ScanFeature/ViewModels/ScanViewModel.swift
+++ b/Sources/ScanFeature/ViewModels/ScanViewModel.swift
@@ -78,7 +78,8 @@ final class ScanViewModel {
                     marshaled: data,
                     username: usernameAndId.0,
                     nickname: nil,
-                    createdAt: Date()
+                    createdAt: Date(),
+                    isRecent: false
                 )
 
                 self.succeed(with: contact)
diff --git a/Sources/ScanFeature/Views/AttributeSwitcher.swift b/Sources/ScanFeature/Views/AttributeSwitcher.swift
index 4c62bdba..4bc957c6 100644
--- a/Sources/ScanFeature/Views/AttributeSwitcher.swift
+++ b/Sources/ScanFeature/Views/AttributeSwitcher.swift
@@ -2,13 +2,45 @@ import UIKit
 import Shared
 
 final class AttributeSwitcher: UIView {
-    let contentLabel = UILabel()
-    let titleLabel = UILabel()
-    let iconImageView = UIImageView()
-    let separatorView = UIView()
-    let switcherView = UISwitch()
-    let stackView = UIStackView()
-    let verticalStackView = UIStackView()
+    struct State {
+        var content: String
+        var isVisible: Bool
+    }
+
+    private let titleLabel = UILabel()
+    private let contentLabel = UILabel()
+    private let stackView = UIStackView()
+    private(set) var switcherView = UISwitch()
+    private let verticalStackView = UIStackView()
+
+    private(set) var addButton: UIControl = {
+        let label = UILabel()
+        let icon = UIImageView()
+        let control = UIControl()
+
+        icon.image = Asset.scanAdd.image
+        label.text = Localized.Scan.Display.Share.add
+        label.textColor = Asset.brandPrimary.color
+
+        control.addSubview(icon)
+        control.addSubview(label)
+
+        icon.snp.makeConstraints {
+            $0.left.equalToSuperview()
+            $0.top.equalToSuperview()
+            $0.bottom.equalToSuperview()
+            $0.width.equalTo(icon.snp.height)
+        }
+
+        label.snp.makeConstraints {
+            $0.left.equalTo(icon.snp.right).offset(5)
+            $0.top.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
+
+        return control
+    }()
 
     public init() {
         super.init(frame: .zero)
@@ -16,60 +48,55 @@ final class AttributeSwitcher: UIView {
         contentLabel.textColor = Asset.neutralActive.color
         titleLabel.textColor = Asset.neutralWeak.color
         switcherView.onTintColor = Asset.brandPrimary.color
-        separatorView.backgroundColor = Asset.neutralLine.color
-
-        iconImageView.contentMode = .center
-        iconImageView.setContentHuggingPriority(.required, for: .horizontal)
 
         contentLabel.numberOfLines = 0
         contentLabel.font = Fonts.Mulish.regular.font(size: 16.0)
         titleLabel.font = Fonts.Mulish.bold.font(size: 12.0)
 
         addSubview(stackView)
-        addSubview(separatorView)
 
-        verticalStackView.spacing = 8
+        verticalStackView.spacing = 5
         verticalStackView.axis = .vertical
         verticalStackView.addArrangedSubview(titleLabel)
         verticalStackView.addArrangedSubview(contentLabel)
 
-        let icon = iconImageView.pinning(at: .top(10))
+        switcherView.setContentCompressionResistancePriority(.required, for: .vertical)
+        switcherView.setContentCompressionResistancePriority(.required, for: .horizontal)
 
-        stackView.spacing = 20
-        stackView.addArrangedSubview(icon)
         stackView.addArrangedSubview(verticalStackView)
-        stackView.addArrangedSubview(switcherView.pinning(at: .top(5)))
+        stackView.addArrangedSubview(FlexibleSpace())
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(26)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview().offset(-25)
-        }
+        let otherHStack = UIStackView()
+        otherHStack.addArrangedSubview(addButton)
+        otherHStack.addArrangedSubview(switcherView)
 
-        separatorView.snp.makeConstraints { make in
-            make.height.equalTo(1)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview()
+        let otherVStack = UIStackView()
+        otherVStack.axis = .vertical
+        otherVStack.addArrangedSubview(otherHStack)
+        otherVStack.addArrangedSubview(FlexibleSpace())
+
+        stackView.addArrangedSubview(otherVStack)
+
+        stackView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
         }
     }
 
     required init?(coder: NSCoder) { nil }
 
-    func set(
-        title: String,
-        text: String,
-        icon: UIImage,
-        separator: Bool = true
-    ) {
+    func setup(state: State?, title: String) {
         titleLabel.text = title
-        contentLabel.text = text
-        iconImageView.image = icon
 
-        guard separator == true else {
-            self.separatorView.removeFromSuperview()
+        guard let state = state else {
+            addButton.isHidden = false
+            switcherView.isHidden = true
+            contentLabel.text = Localized.Scan.Display.Share.notAdded
             return
         }
+
+        addButton.isHidden = true
+        switcherView.isHidden = false
+        switcherView.isOn = state.isVisible
+        contentLabel.text = state.isVisible ? state.content : Localized.Scan.Display.Share.hidden
     }
 }
diff --git a/Sources/ScanFeature/Views/ScanContainerView.swift b/Sources/ScanFeature/Views/ScanContainerView.swift
index 5e832828..b9b35cef 100644
--- a/Sources/ScanFeature/Views/ScanContainerView.swift
+++ b/Sources/ScanFeature/Views/ScanContainerView.swift
@@ -3,41 +3,26 @@ import Shared
 
 final class ScanContainerView: UIView {
     let scrollView = UIScrollView()
-    let scanScreen = ScanController()
     let segmentedControl = SegmentedControl()
-    let displayScreen = ScanDisplayController()
 
     init() {
         super.init(frame: .zero)
 
-        backgroundColor = Asset.neutralActive.color
+        backgroundColor = Asset.neutralDark.color
         addSubview(segmentedControl)
         addSubview(scrollView)
 
-        scrollView.addSubview(scanScreen.view)
-        scrollView.addSubview(displayScreen.view)
-
-        setupConstraints()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    private func setupConstraints() {
-        scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
-
-        displayScreen.view.snp.makeConstraints { $0.top.bottom.width.equalTo(scanScreen.view) }
-
-        segmentedControl.snp.makeConstraints { make in
-            make.top.equalTo(safeAreaLayoutGuide).offset(40)
-            make.centerX.equalToSuperview()
-            make.height.equalTo(30)
+        scrollView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
         }
 
-        scanScreen.view.snp.makeConstraints { make in
-            make.top.equalTo(self)
-            make.bottom.width.equalTo(self)
-            make.right.equalTo(displayScreen.view.snp.left)
-            make.left.equalToSuperview()
+        segmentedControl.snp.makeConstraints {
+            $0.top.equalTo(safeAreaLayoutGuide).offset(10)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.height.equalTo(60)
         }
     }
+
+    required init?(coder: NSCoder) { nil }
 }
diff --git a/Sources/ScanFeature/Views/ScanDisplayShareView.swift b/Sources/ScanFeature/Views/ScanDisplayShareView.swift
index 79030c63..acad98f7 100644
--- a/Sources/ScanFeature/Views/ScanDisplayShareView.swift
+++ b/Sources/ScanFeature/Views/ScanDisplayShareView.swift
@@ -1,81 +1,198 @@
 import UIKit
 import Shared
+import SnapKit
+import Combine
 
 final class ScanDisplayShareView: UIView {
-    let stackView = UIStackView()
-    let textWithInfo = TextWithInfoView()
-    let emailView = AttributeSwitcher()
-    let phoneView = AttributeSwitcher()
+    enum Action {
+        case info
+        case addEmail
+        case addPhone
+        case toggleEmail
+        case togglePhone
+    }
+
+    private var isExpanded = false {
+        didSet { updateBottomConstraint() }
+    }
+
+    private let upperView = UIView()
+    private let lowerView = UIView()
+    private var bottomConstraint: Constraint?
 
-    var didTapInfo: (() -> Void)?
+    private let imageView = UIImageView()
+    private let titleView = TextWithInfoView()
+    private let emailView = AttributeSwitcher()
+    private let phoneView = AttributeSwitcher()
+    private var cancellables = Set<AnyCancellable>()
+
+    private var currentConstraintConstant: CGFloat = 0.0 {
+        didSet { bottomConstraint?.update(offset: currentConstraintConstant) }
+    }
+
+    private var bottomConstraintExpanded: CGFloat {
+        -lowerView.frame.height
+    }
+
+    private var bottomConstraintNotExpanded: CGFloat {
+        0
+    }
+
+    var actionPublisher: AnyPublisher<Action, Never> {
+        actionSubject.eraseToAnyPublisher()
+    }
+
+    private let actionSubject = PassthroughSubject<Action, Never>()
 
     init() {
         super.init(frame: .zero)
+
+        upperView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:))))
+        lowerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(_:))))
+
+        layer.cornerRadius = 30
+        imageView.image = Asset.scanDropdown.image
         backgroundColor = Asset.neutralWhite.color
+        clipsToBounds = true
+        layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
 
-        let titleContainer = UIView()
-        titleContainer.addSubview(textWithInfo)
+        addSubview(upperView)
+        addSubview(lowerView)
+
+        upperView.addSubview(imageView)
+        upperView.addSubview(titleView)
+        lowerView.addSubview(emailView)
+        lowerView.addSubview(phoneView)
 
         let paragraphStyle = NSMutableParagraphStyle()
         paragraphStyle.lineBreakMode = .byWordWrapping
 
-        textWithInfo.setup(
+        titleView.setup(
             text: Localized.Scan.Display.Share.title,
             attributes: [
                 .foregroundColor: Asset.neutralBody.color,
                 .font: Fonts.Mulish.regular.font(size: 16.0) as Any,
                 .paragraphStyle: paragraphStyle
             ],
-            didTapInfo: { [weak self] in self?.didTapInfo?() }
+            didTapInfo: { [weak self] in self?.actionSubject.send(.info) }
         )
 
-        stackView.spacing = 5
-        stackView.axis = .vertical
-        stackView.addArrangedSubview(titleContainer)
+        emailView.switcherView
+            .publisher(for: .valueChanged)
+            .sink { [unowned self] in actionSubject.send(.toggleEmail) }
+            .store(in: &cancellables)
+
+        phoneView.switcherView
+            .publisher(for: .valueChanged)
+            .sink { [unowned self] in actionSubject.send(.togglePhone) }
+            .store(in: &cancellables)
+
+        emailView.addButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.addEmail) }
+            .store(in: &cancellables)
+
+        phoneView.addButton
+            .publisher(for: .touchUpInside)
+            .sink { [unowned self] in actionSubject.send(.addPhone) }
+            .store(in: &cancellables)
+
+        emailView.setup(state: nil, title: Localized.Scan.Display.Share.email)
+        phoneView.setup(state: nil, title: Localized.Scan.Display.Share.phone)
+        emailView.alpha = 0.0
+        phoneView.alpha = 0.0
+
+        imageView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(15)
+            $0.centerX.equalToSuperview()
+        }
 
-        addSubview(stackView)
+        titleView.snp.makeConstraints {
+            $0.top.equalTo(imageView.snp.bottom).offset(10)
+            $0.left.equalToSuperview().offset(40)
+            $0.right.lessThanOrEqualToSuperview().offset(-40)
+            $0.centerY.equalToSuperview()
+        }
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(22)
-            make.left.equalToSuperview().offset(22)
-            make.right.equalToSuperview().offset(-22)
-            make.bottom.equalToSuperview().offset(-50)
+        emailView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview().offset(40)
+            $0.right.equalToSuperview().offset(-40)
         }
 
-        textWithInfo.snp.makeConstraints { make in
-            make.top.greaterThanOrEqualToSuperview()
-            make.centerY.equalToSuperview()
-            make.bottom.lessThanOrEqualToSuperview()
-            make.left.equalToSuperview().offset(5)
-            make.right.lessThanOrEqualToSuperview().offset(27)
-            make.height.equalTo(30)
+        phoneView.snp.makeConstraints {
+            $0.top.equalTo(emailView.snp.bottom).offset(25)
+            $0.left.equalToSuperview().offset(40)
+            $0.right.equalToSuperview().offset(-40)
+            $0.bottom.equalToSuperview().offset(-40)
         }
-    }
 
-    required init?(coder: NSCoder) { nil }
+        upperView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            bottomConstraint = $0.bottom
+                .equalTo(safeAreaLayoutGuide)
+                .constraint
+        }
 
-    func setup(email: String) -> UIControl.EventPublisher {
-        stackView.addArrangedSubview(emailView)
+        lowerView.snp.makeConstraints {
+            $0.top.equalTo(upperView.snp.bottom).offset(-30)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+        }
+    }
 
-        emailView.set(
-            title: Localized.Scan.Display.Share.email,
-            text: email,
-            icon: Asset.scanEmail.image
-        )
+    required init?(coder: NSCoder) { nil }
 
-        return emailView.switcherView.publisher(for: .valueChanged)
+    func setup(email state: AttributeSwitcher.State?) {
+        emailView.setup(state: state, title: Localized.Scan.Display.Share.email)
     }
 
-    func setup(phone: String) -> UIControl.EventPublisher {
-        stackView.addArrangedSubview(phoneView)
+    func setup(phone state: AttributeSwitcher.State?) {
+        phoneView.setup(state: state, title: Localized.Scan.Display.Share.phone)
+    }
 
-        phoneView.set(
-            title: Localized.Scan.Display.Share.phone,
-            text: phone,
-            icon: Asset.scanPhone.image,
-            separator: false
-        )
+    @objc private func didPan(_ sender: UIPanGestureRecognizer) {
+        switch sender.state {
+        case .began, .changed:
+            let isUpwards = sender.translation(in: self).y < 0
+            let result = currentConstraintConstant + sender.translation(in: self).y
+
+            if isUpwards {
+                currentConstraintConstant = max(bottomConstraintExpanded, result)
+            } else {
+                currentConstraintConstant = min(bottomConstraintNotExpanded, result)
+            }
+
+            let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded
+            let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded
+            let alpha = 1 - (currentMinusExpanded / abs(notExpandedMinusExpanded))
+            emailView.alpha = alpha
+            phoneView.alpha = alpha
+
+        case .cancelled, .ended, .failed:
+            let currentMinusExpanded = currentConstraintConstant - bottomConstraintExpanded
+            let notExpandedMinusExpanded = bottomConstraintNotExpanded - bottomConstraintExpanded
+            let percentage = currentMinusExpanded / abs(notExpandedMinusExpanded)
+            isExpanded = percentage < 0.5
+
+        case .possible:
+            break
+        @unknown default:
+            break
+        }
+    }
 
-        return phoneView.switcherView.publisher(for: .valueChanged)
+    private func updateBottomConstraint() {
+        if isExpanded {
+            emailView.alpha = 1.0
+            phoneView.alpha = 1.0
+            currentConstraintConstant = bottomConstraintExpanded
+        } else {
+            emailView.alpha = 0.0
+            phoneView.alpha = 0.0
+            currentConstraintConstant = bottomConstraintNotExpanded
+        }
     }
 }
diff --git a/Sources/ScanFeature/Views/ScanDisplayView.swift b/Sources/ScanFeature/Views/ScanDisplayView.swift
index c1a1b23b..fa2b0243 100644
--- a/Sources/ScanFeature/Views/ScanDisplayView.swift
+++ b/Sources/ScanFeature/Views/ScanDisplayView.swift
@@ -1,42 +1,101 @@
 import UIKit
 import Shared
+import Combine
 
 final class ScanDisplayView: UIView {
-    let qrView = UIView()
-    let qrImage = UIImageView()
-    let shareView = ScanDisplayShareView()
+    var actionPublisher: AnyPublisher<ScanDisplayShareView.Action, Never> {
+        shareSheetView.actionPublisher.eraseToAnyPublisher()
+    }
+
+    private let copyLabel = UILabel()
+    private let codeButton = ScanQRButton()
+    private let copyImageView = UIImageView()
+    private let copyContainerButton = UIControl()
+    private var cancellables = Set<AnyCancellable>()
+    private let shareSheetView = ScanDisplayShareView()
 
     init() {
         super.init(frame: .zero)
         backgroundColor = Asset.neutralDark.color
 
-        qrView.backgroundColor = Asset.neutralWhite.color
-        qrView.layer.cornerRadius = 30
+        copyImageView.image = Asset.scanCopy.image
+        copyLabel.text = Localized.Scan.Display.copy
+        copyLabel.textColor = Asset.neutralDisabled.color
+        copyLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
+
+        codeButton.publisher(for: .touchUpInside)
+            .merge(with: copyContainerButton.publisher(for: .touchUpInside))
+            .sink { [unowned self] in
+                UIGraphicsBeginImageContext(codeButton.frame.size)
+                codeButton.layer.render(in: UIGraphicsGetCurrentContext()!)
+                let output = UIGraphicsGetImageFromCurrentImageContext()
+                UIGraphicsEndImageContext()
+
+                UIImageWriteToSavedPhotosAlbum(output!, nil, nil, nil)
+                codeButton.blinkCopied()
+            }.store(in: &cancellables)
+
+        addSubview(codeButton)
+        addSubview(copyContainerButton)
+        copyContainerButton.addSubview(copyLabel)
+        copyContainerButton.addSubview(copyImageView)
+
+        addSubview(shareSheetView)
+
+        codeButton.snp.makeConstraints {
+            $0.centerX.equalTo(safeAreaLayoutGuide)
+            $0.centerY.equalTo(safeAreaLayoutGuide).multipliedBy(0.6)
+            $0.width.equalTo(safeAreaLayoutGuide).multipliedBy(0.6)
+            $0.height.equalTo(codeButton.snp.width)
+        }
+
+        copyContainerButton.snp.makeConstraints {
+            $0.top.equalTo(codeButton.snp.bottom).offset(33)
+            $0.centerX.equalTo(codeButton)
+        }
 
-        addSubview(qrView)
-        qrView.addSubview(qrImage)
+        copyImageView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.bottom.equalToSuperview()
+        }
 
-        qrView.snp.makeConstraints { make in
-            make.width.height.equalTo(207)
-            make.centerX.equalToSuperview()
-            make.centerY.equalToSuperview().multipliedBy(0.8)
+        copyLabel.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalTo(copyImageView.snp.right).offset(5)
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
         }
 
-        qrImage.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-            make.left.top.equalToSuperview().offset(20)
+        shareSheetView.snp.makeConstraints {
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
         }
     }
 
     required init?(coder: NSCoder) { nil }
 
-    func setupShareView(info: @escaping () -> Void) {
-        addSubview(shareView)
-        shareView.didTapInfo = info
+    func setup(code image: CIImage) {
+        codeButton.setup(code: image)
+    }
+
+    func setupAttributes(
+        email: String?,
+        phone: String?,
+        emailSharing: Bool,
+        phoneSharing: Bool
+    ) {
+        if let email = email {
+            shareSheetView.setup(email: .init(content: email, isVisible: emailSharing))
+        } else {
+            shareSheetView.setup(email: nil)
+        }
 
-        shareView.snp.makeConstraints { make in
-            make.left.right.equalToSuperview()
-            make.bottom.equalTo(safeAreaLayoutGuide)
+        if let phone = phone {
+            shareSheetView.setup(phone: .init(content: phone, isVisible: phoneSharing))
+        } else {
+            shareSheetView.setup(phone: nil)
         }
     }
 }
diff --git a/Sources/ScanFeature/Views/ScanOverlayView.swift b/Sources/ScanFeature/Views/ScanOverlayView.swift
index 995e7eb2..14bc4c5d 100644
--- a/Sources/ScanFeature/Views/ScanOverlayView.swift
+++ b/Sources/ScanFeature/Views/ScanOverlayView.swift
@@ -2,13 +2,13 @@ import UIKit
 import Shared
 
 final class ScanOverlayView: UIView {
-    let cropView = UIView()
-    let maskLayer = CAShapeLayer()
-
-    let topLeftLayer = CAShapeLayer()
-    let topRightLayer = CAShapeLayer()
-    let bottomLeftLayer = CAShapeLayer()
-    let bottomRightLayer = CAShapeLayer()
+    private let cropView = UIView()
+    private let scanViewLength = 266.0
+    private let maskLayer = CAShapeLayer()
+    private let topLeftLayer = CAShapeLayer()
+    private let topRightLayer = CAShapeLayer()
+    private let bottomLeftLayer = CAShapeLayer()
+    private let bottomRightLayer = CAShapeLayer()
 
     init() {
         super.init(frame: .zero)
@@ -16,9 +16,11 @@ final class ScanOverlayView: UIView {
 
         addSubview(cropView)
 
-        cropView.snp.makeConstraints { make in
-            make.center.equalToSuperview()
-            make.width.height.equalTo(207)
+        cropView.snp.makeConstraints {
+            $0.width.equalTo(scanViewLength)
+            $0.centerY.equalToSuperview().offset(-50)
+            $0.centerX.equalToSuperview()
+            $0.height.equalTo(scanViewLength)
         }
 
         maskLayer.fillRule = .evenOdd
@@ -63,38 +65,105 @@ final class ScanOverlayView: UIView {
 
     func topLeftPath() -> CGPath {
         let path = UIBezierPath()
-        path.addArc(
-            center: CGPoint(x: cropView.frame.minX + 10, y: cropView.frame.minY + 10),
-            startAngle: .pi
-        )
+
+        let vert0X = cropView.frame.minX - 15
+        let vert0Y = cropView.frame.minY + 45
+        let vert0 = CGPoint(x: vert0X, y: vert0Y)
+        path.move(to: vert0)
+
+        let vertNX = cropView.frame.minX - 15
+        let vertNY = cropView.frame.minY + 15
+        let vertN = CGPoint(x: vertNX, y: vertNY)
+        path.addLine(to: vertN)
+
+        let arcCenterX = cropView.frame.minX + 15
+        let arcCenterY = cropView.frame.minY + 15
+        let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: .pi)
+
+        let horizX = cropView.frame.minX + 45
+        let horizY = cropView.frame.minY - 15
+        let horiz = CGPoint(x: horizX, y: horizY)
+        path.addLine(to: horiz)
+
         return path.cgPath
     }
 
     func topRightPath() -> CGPath {
         let path = UIBezierPath()
-        path.addArc(
-            center: CGPoint(x: cropView.frame.maxX - 10, y: cropView.frame.minY + 10),
-            startAngle: 3 * .pi/2
-        )
+
+        let horiz0X = cropView.frame.maxX - 45
+        let horiz0Y = cropView.frame.minY - 15
+        let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
+        path.move(to: horiz0)
+
+        let horizNX = cropView.frame.maxX - 15
+        let horizNY = cropView.frame.minY - 15
+        let horizN = CGPoint(x: horizNX, y: horizNY)
+        path.addLine(to: horizN)
+
+        let arcCenterX = cropView.frame.maxX - 15
+        let arcCenterY = cropView.frame.minY + 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: 3 * .pi/2)
+
+        let vertX = cropView.frame.maxX + 15
+        let vertY = cropView.frame.minY + 45
+        let vert = CGPoint(x: vertX, y: vertY)
+        path.addLine(to: vert)
+
         return path.cgPath
     }
 
     func bottomRightPath() -> CGPath {
         let path = UIBezierPath()
-        path.addArc(
-            center: CGPoint(x: cropView.frame.maxX - 10, y: cropView.frame.maxY - 10),
-            startAngle: 0
-        )
+
+        let vert0X = cropView.frame.maxX + 15
+        let vert0Y = cropView.frame.maxY - 45
+        let vert0 = CGPoint(x: vert0X, y: vert0Y)
+        path.move(to: vert0)
+
+        let vertNX = cropView.frame.maxX + 15
+        let vertNY = cropView.frame.maxY - 15
+        let vertN = CGPoint(x: vertNX, y: vertNY)
+        path.addLine(to: vertN)
+
+        let arcCenterX = cropView.frame.maxX - 15
+        let arcCenterY = cropView.frame.maxY - 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: 0)
+
+        let horizX = cropView.frame.maxX - 45
+        let horizY = cropView.frame.maxY + 15
+        let horiz = CGPoint(x: horizX, y: horizY)
+        path.addLine(to: horiz)
 
         return path.cgPath
     }
 
     func bottomLeftPath() -> CGPath {
         let path = UIBezierPath()
-        path.addArc(
-            center: CGPoint(x: cropView.frame.minX + 10, y: cropView.frame.maxY - 10),
-            startAngle: .pi/2
-        )
+
+        let horiz0X = cropView.frame.minX + 45
+        let horiz0Y = cropView.frame.maxY + 15
+        let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
+        path.move(to: horiz0)
+
+        let horizNX = cropView.frame.minX + 15
+        let horizNY = cropView.frame.maxY + 15
+        let horizN = CGPoint(x: horizNX, y: horizNY)
+        path.addLine(to: horizN)
+
+        let arcCenterX = cropView.frame.minX + 15
+        let arcCenterY = cropView.frame.maxY - 15
+        let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
+        path.addArc(center: arcCenter, startAngle: .pi/2)
+
+        let vertX = cropView.frame.minX - 15
+        let vertY = cropView.frame.maxY - 45
+        let vert = CGPoint(x: vertX, y: vertY)
+        path.addLine(to: vert)
+
         return path.cgPath
     }
 }
diff --git a/Sources/ScanFeature/Views/ScanQRButton.swift b/Sources/ScanFeature/Views/ScanQRButton.swift
new file mode 100644
index 00000000..66d911c6
--- /dev/null
+++ b/Sources/ScanFeature/Views/ScanQRButton.swift
@@ -0,0 +1,59 @@
+import UIKit
+import Shared
+
+final class ScanQRButton: UIControl {
+    private let overlayView = UIView()
+    private let copiedLabel = UILabel()
+    private(set) var imageView = UIImageView()
+
+    init() {
+        super.init(frame: .zero)
+
+        clipsToBounds = true
+        overlayView.alpha = 0.0
+        layer.cornerRadius = 30
+        backgroundColor = Asset.neutralWhite.color
+        overlayView.backgroundColor = Asset.brandDefault.color.withAlphaComponent(0.9)
+
+        copiedLabel.text = Localized.Scan.Display.copied
+        copiedLabel.textColor = Asset.neutralWhite.color
+        copiedLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
+
+        addSubview(imageView)
+        addSubview(overlayView)
+        overlayView.addSubview(copiedLabel)
+
+        copiedLabel.snp.makeConstraints {
+            $0.center.equalToSuperview()
+        }
+
+        imageView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(20)
+            $0.left.equalToSuperview().offset(20)
+            $0.right.equalToSuperview().offset(-20)
+            $0.bottom.equalToSuperview().offset(-20)
+        }
+
+        overlayView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+    }
+
+    required init?(coder: NSCoder) { nil }
+
+    func setup(code image: CIImage) {
+        imageView.image = UIImage(ciImage: image)
+    }
+
+    func blinkCopied() {
+        UIView.animateKeyframes(withDuration: 1.0, delay: 0) {
+            UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1) {
+                self.overlayView.alpha = 1.0
+            }
+
+            UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.2) {
+                self.overlayView.alpha = 0.0
+            }
+        }
+    }
+}
diff --git a/Sources/ScanFeature/Views/ScanView.swift b/Sources/ScanFeature/Views/ScanView.swift
index a21ceb85..7aff4cf2 100644
--- a/Sources/ScanFeature/Views/ScanView.swift
+++ b/Sources/ScanFeature/Views/ScanView.swift
@@ -2,16 +2,16 @@ import UIKit
 import Shared
 
 final class ScanView: UIView {
-    let overlay = ScanOverlayView()
-    let animationView = DotAnimation()
-    let iconImageView = UIImageView()
-    let statusLabel = UILabel()
-    let actionButton = CapsuleButton()
-    let stackView = UIStackView()
+    private let statusLabel = UILabel()
+    private let imageView = UIImageView()
+    private let stackView = UIStackView()
+    private let animationView = DotAnimation()
+    private let overlayView = ScanOverlayView()
+    private(set) var actionButton = CapsuleButton()
 
     init() {
         super.init(frame: .zero)
-        iconImageView.contentMode = .center
+        imageView.contentMode = .center
         actionButton.setStyle(.brandColored)
 
         statusLabel.numberOfLines = 0
@@ -22,28 +22,28 @@ final class ScanView: UIView {
         stackView.spacing = 15
         stackView.axis = .vertical
         stackView.addArrangedSubview(animationView)
-        stackView.addArrangedSubview(iconImageView)
+        stackView.addArrangedSubview(imageView)
         stackView.addArrangedSubview(statusLabel)
         stackView.addArrangedSubview(actionButton)
 
-        animationView.isHidden = false
-        iconImageView.isHidden = true
+        imageView.isHidden = true
         actionButton.isHidden = true
+        animationView.isHidden = false
 
-        addSubview(overlay)
+        addSubview(overlayView)
         addSubview(stackView)
 
-        overlay.snp.makeConstraints { make in
-            make.top.equalToSuperview()
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview()
+        overlayView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
         }
 
-        stackView.snp.makeConstraints { make in
-            make.left.equalToSuperview().offset(57)
-            make.right.equalToSuperview().offset(-57)
-            make.bottom.equalTo(safeAreaLayoutGuide).offset(-100)
+        stackView.snp.makeConstraints {
+            $0.left.equalToSuperview().offset(57)
+            $0.right.equalToSuperview().offset(-57)
+            $0.bottom.equalTo(safeAreaLayoutGuide).offset(-100)
         }
     }
 
@@ -54,24 +54,24 @@ final class ScanView: UIView {
 
         switch state {
         case .reading, .processing:
-            iconImageView.isHidden = true
+            imageView.isHidden = true
             actionButton.isHidden = true
             text = Localized.Scan.Status.reading
-            overlay.updateCornerColor(Asset.brandPrimary.color)
+            overlayView.updateCornerColor(Asset.brandPrimary.color)
 
         case .success:
             animationView.isHidden = true
             actionButton.isHidden = true
-            iconImageView.isHidden = false
-            iconImageView.image = Asset.sharedSuccess.image
+            imageView.isHidden = false
+            imageView.image = Asset.sharedSuccess.image
             text = Localized.Scan.Status.success
-            overlay.updateCornerColor(Asset.accentSuccess.color)
+            overlayView.updateCornerColor(Asset.accentSuccess.color)
 
         case .failed(let error):
             animationView.isHidden = true
-            iconImageView.image = Asset.scanError.image
-            iconImageView.isHidden = false
-            overlay.updateCornerColor(Asset.accentDanger.color)
+            imageView.image = Asset.scanError.image
+            imageView.isHidden = false
+            overlayView.updateCornerColor(Asset.accentDanger.color)
 
             switch error {
             case .requestOpened:
@@ -101,7 +101,7 @@ final class ScanView: UIView {
 
         attString.addAttribute(.paragraphStyle, value: paragraph)
         attString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color)
-        attString.addAttribute(.font, value: Fonts.Mulish.semiBold.font(size: 18.0) as Any)
+        attString.addAttribute(.font, value: Fonts.Mulish.regular.font(size: 14.0) as Any)
 
         if text.contains("#") {
             attString.addAttribute(name: .foregroundColor, value: Asset.brandPrimary.color, betweenCharacters: "#")
diff --git a/Sources/ScanFeature/Views/SegmentedControl.swift b/Sources/ScanFeature/Views/SegmentedControl.swift
index 3b9d9a5e..11faf5a5 100644
--- a/Sources/ScanFeature/Views/SegmentedControl.swift
+++ b/Sources/ScanFeature/Views/SegmentedControl.swift
@@ -3,26 +3,32 @@ import Shared
 import SnapKit
 
 final class SegmentedControl: UIView {
-    let trackView = UIView()
-    let trackIndicatorView = UIView()
-    var leftConstraint: Constraint?
-    let leftButton = SegmentedControlButton()
-    let rightButton = SegmentedControlButton()
-    let stackView = UIStackView()
+    private let trackHeight = 2.0
+    private let numberOfTabs = 2.0
+    private let trackView = UIView()
+    private let stackView = UIStackView()
+    private var leftConstraint: Constraint?
+    private let trackIndicatorView = UIView()
+    private(set) var leftButton = SegmentedControlButton()
+    private(set) var rightButton = SegmentedControlButton()
 
     init() {
         super.init(frame: .zero)
 
-        rightButton.icon.image = Asset.scanQr.image
-        leftButton.title.text = Localized.Scan.SegmentedControl.left
-        rightButton.title.text = Localized.Scan.SegmentedControl.right
+        rightButton.setup(
+            title: Localized.Scan.SegmentedControl.right,
+            icon: Asset.scanQr.image
+        )
 
-        leftButton.title.font = Fonts.Mulish.semiBold.font(size: 14.0)
-        rightButton.title.font = Fonts.Mulish.semiBold.font(size: 14.0)
+        leftButton.setup(
+            title: Localized.Scan.SegmentedControl.left,
+            icon: Asset.scanScan.image
+        )
 
+        trackView.backgroundColor = Asset.neutralLine.color
         trackIndicatorView.backgroundColor = Asset.brandPrimary.color
 
-        stackView.spacing = 40
+        stackView.distribution = .fillEqually
         stackView.addArrangedSubview(leftButton)
         stackView.addArrangedSubview(rightButton)
 
@@ -30,30 +36,45 @@ final class SegmentedControl: UIView {
         addSubview(trackView)
         trackView.addSubview(trackIndicatorView)
 
-        stackView.snp.makeConstraints { make in
-            make.top.equalTo(trackView.snp.bottom).offset(2)
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview()
+        stackView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalTo(trackView.snp.top)
         }
 
-        trackView.snp.makeConstraints { make in
-            make.top.equalToSuperview()
-            make.left.equalTo(stackView)
-            make.right.equalTo(stackView)
-            make.height.equalTo(3)
+        trackView.snp.makeConstraints {
+            $0.height.equalTo(trackHeight)
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
         }
 
-        trackIndicatorView.snp.makeConstraints { make in
-            leftConstraint = make.left.equalToSuperview().constraint
-            make.top.bottom.equalToSuperview()
-            make.width.equalTo(75)
+        trackIndicatorView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            leftConstraint = $0.left.equalToSuperview().constraint
+            $0.width.equalToSuperview().dividedBy(numberOfTabs)
+            $0.bottom.equalToSuperview()
         }
     }
 
     required init?(coder: NSCoder) { nil }
 
-    func updateLeftConstraint(_ percentage: CGFloat) {
-        leftConstraint?.update(offset: percentage * 125)
+    func updateLeftConstraint(_ percentageScrolled: CGFloat) {
+        let tabWidth = bounds.width / numberOfTabs
+        let leftOffset = percentageScrolled * tabWidth
+        leftConstraint?.update(offset: leftOffset)
+
+        leftButton.update(color: .fade(
+            from: Asset.brandPrimary.color,
+            to: Asset.neutralLine.color,
+            pcent: percentageScrolled
+        ))
+
+        rightButton.update(color: .fade(
+            from: Asset.brandPrimary.color,
+            to: Asset.neutralLine.color,
+            pcent: 1 - percentageScrolled
+        ))
     }
 }
diff --git a/Sources/ScanFeature/Views/SegmentedControlButton.swift b/Sources/ScanFeature/Views/SegmentedControlButton.swift
index 8270a194..c23f16c1 100644
--- a/Sources/ScanFeature/Views/SegmentedControlButton.swift
+++ b/Sources/ScanFeature/Views/SegmentedControlButton.swift
@@ -2,28 +2,40 @@ import UIKit
 import Shared
 
 final class SegmentedControlButton: UIControl {
-    let title = UILabel()
-    let icon = UIImageView()
-    let stack = UIStackView()
+    private let titleLabel = UILabel()
+    private let imageView = UIImageView()
 
     init() {
         super.init(frame: .zero)
 
-        title.textColor = Asset.neutralWhite.color
-        title.font = Fonts.Mulish.bold.font(size: 15.0)
+        titleLabel.textAlignment = .center
+        titleLabel.textColor = Asset.neutralWhite.color
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
 
-        addSubview(icon)
-        addSubview(title)
+        addSubview(titleLabel)
+        addSubview(imageView)
 
-        stack.spacing = 6
-        stack.addArrangedSubview(icon)
-        stack.addArrangedSubview(title)
-        stack.isUserInteractionEnabled = false
+        imageView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(7.5)
+            $0.centerX.equalToSuperview()
+        }
 
-        addSubview(stack)
-
-        stack.snp.makeConstraints { $0.edges.equalToSuperview() }
+        titleLabel.snp.makeConstraints {
+            $0.top.equalTo(imageView.snp.bottom).offset(2)
+            $0.centerX.equalToSuperview()
+            $0.bottom.equalToSuperview().offset(-7.5)
+        }
     }
 
     required init?(coder: NSCoder) { nil }
+
+    func setup(title: String, icon: UIImage) {
+        titleLabel.text = title
+        imageView.image = icon
+    }
+
+    func update(color: UIColor) {
+        imageView.tintColor = color
+        titleLabel.textColor = color
+    }
 }
diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift
index f750fc38..7702558f 100644
--- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift
+++ b/Sources/SearchFeature/ViewModels/SearchViewModel.swift
@@ -6,7 +6,7 @@ import Defaults
 import Countries
 import Foundation
 import Integration
-import PushNotifications
+import PushFeature
 import CombineSchedulers
 import DependencyInjection
 
@@ -143,7 +143,7 @@ final class SearchViewModel {
     private func verifyNotifications() {
         guard pushNotifications == false else { return }
 
-        pushHandler.didRequestAuthorization { [weak self] result in
+        pushHandler.requestAuthorization { [weak self] result in
             guard let self = self else { return }
 
             switch result {
diff --git a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift
index 1645d25a..aeb6791e 100644
--- a/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift
+++ b/Sources/SettingsFeature/Controllers/SettingsAdvancedController.swift
@@ -56,6 +56,11 @@ public final class SettingsAdvancedController: UIViewController {
             .sink { [weak viewModel] in viewModel?.didToggleRecordLogs() }
             .store(in: &cancellables)
 
+        screenView.showUsernamesSwitcher.switcherView
+            .publisher(for: .valueChanged)
+            .sink { [weak viewModel] in viewModel?.didToggleShowUsernames() }
+            .store(in: &cancellables)
+
         screenView.crashReportingSwitcher.switcherView
             .publisher(for: .valueChanged)
             .sink { [weak viewModel] in viewModel?.didToggleCrashReporting() }
@@ -71,6 +76,7 @@ public final class SettingsAdvancedController: UIViewController {
             .sink { [unowned self] state in
                 screenView.logRecordingSwitcher.switcherView.setOn(state.isRecordingLogs, animated: true)
                 screenView.crashReportingSwitcher.switcherView.setOn(state.isCrashReporting, animated: true)
+                screenView.showUsernamesSwitcher.switcherView.setOn(state.isShowingUsernames, animated: true)
             }.store(in: &cancellables)
     }
 
diff --git a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift
index ed304656..87170244 100644
--- a/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/SettingsAdvancedViewModel.swift
@@ -8,12 +8,15 @@ import DependencyInjection
 struct AdvancedViewState: Equatable {
     var isRecordingLogs = false
     var isCrashReporting = false
+    var isShowingUsernames = false
 }
 
 final class SettingsAdvancedViewModel {
     @KeyObject(.recordingLogs, defaultValue: true) var isRecordingLogs: Bool
     @KeyObject(.crashReporting, defaultValue: true) var isCrashReporting: Bool
 
+    private let isShowingUsernamesKey = "isShowingUsernames"
+
     @Dependency private var logger: XXLogger
     @Dependency private var crashReporter: CrashReporter
 
@@ -26,6 +29,29 @@ final class SettingsAdvancedViewModel {
     func loadCachedSettings() {
         stateRelay.value.isRecordingLogs = isRecordingLogs
         stateRelay.value.isCrashReporting = isCrashReporting
+
+        guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else {
+            print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)")
+            return
+        }
+
+        guard let isShowingUsernames = defaults.value(forKey: isShowingUsernamesKey) as? Bool else {
+            defaults.set(false, forKey: isShowingUsernamesKey)
+            return
+        }
+
+        stateRelay.value.isShowingUsernames = isShowingUsernames
+    }
+
+    func didToggleShowUsernames() {
+        stateRelay.value.isShowingUsernames.toggle()
+
+        guard let defaults = UserDefaults(suiteName: "group.elixxir.messenger") else {
+            print("^^^ Couldn't access user defaults in the app group container \(#file):\(#line)")
+            return
+        }
+
+        defaults.set(stateRelay.value.isShowingUsernames, forKey: isShowingUsernamesKey)
     }
 
     func didToggleRecordLogs() {
diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
index 9eb30e13..97c146a6 100644
--- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
+++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift
@@ -5,7 +5,7 @@ import Combine
 import Defaults
 import Permissions
 import Integration
-import PushNotifications
+import PushFeature
 import UserNotifications
 import CombineSchedulers
 import DependencyInjection
@@ -113,7 +113,7 @@ final class SettingsViewModel {
         hudRelay.send(.on(nil))
 
         if enable == true {
-            pushHandler.didRequestAuthorization { [weak self] result in
+            pushHandler.requestAuthorization { [weak self] result in
                 guard let self = self else { return }
 
                 switch result {
diff --git a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift
index e7f96ee8..fb3df7c7 100644
--- a/Sources/SettingsFeature/Views/SettingsAdvancedView.swift
+++ b/Sources/SettingsFeature/Views/SettingsAdvancedView.swift
@@ -6,6 +6,7 @@ final class SettingsAdvancedView: UIView {
     let downloadLogsButton = UIButton()
     let logRecordingSwitcher = SettingsSwitcher()
     let crashReportingSwitcher = SettingsSwitcher()
+    let showUsernamesSwitcher = SettingsSwitcher()
 
     init() {
         super.init(frame: .zero)
@@ -13,6 +14,12 @@ final class SettingsAdvancedView: UIView {
         backgroundColor = Asset.neutralWhite.color
         downloadLogsButton.setImage(Asset.settingsDownload.image, for: .normal)
 
+        showUsernamesSwitcher.set(
+            title: Localized.Settings.Advanced.ShowUsername.title,
+            text: Localized.Settings.Advanced.ShowUsername.description,
+            icon: Asset.settingsHide.image
+        )
+
         logRecordingSwitcher.set(
             title: Localized.Settings.Advanced.Logs.title,
             text: Localized.Settings.Advanced.Logs.description,
@@ -29,9 +36,11 @@ final class SettingsAdvancedView: UIView {
         stackView.axis = .vertical
         stackView.addArrangedSubview(logRecordingSwitcher)
         stackView.addArrangedSubview(crashReportingSwitcher)
+        stackView.addArrangedSubview(showUsernamesSwitcher)
 
         stackView.setCustomSpacing(20, after: logRecordingSwitcher)
         stackView.setCustomSpacing(10, after: crashReportingSwitcher)
+        stackView.setCustomSpacing(10, after: showUsernamesSwitcher)
 
         addSubview(stackView)
 
diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/Shared/AutoGenerated/Assets.swift
index 76164e8f..2b2248fb 100644
--- a/Sources/Shared/AutoGenerated/Assets.swift
+++ b/Sources/Shared/AutoGenerated/Assets.swift
@@ -45,8 +45,10 @@ public enum Asset {
   public static let chatListMenuDelete = ImageAsset(name: "chat_list_menu_delete")
   public static let chatListMenuPin = ImageAsset(name: "chat_list_menu_pin")
   public static let chatListNew = ImageAsset(name: "chat_list_new")
+  public static let chatListNewGroup = ImageAsset(name: "chat_list_new_group")
   public static let chatListPinSwipe = ImageAsset(name: "chat_list_pin_swipe")
   public static let chatListPlaceholder = ImageAsset(name: "chat_list_placeholder")
+  public static let chatListUd = ImageAsset(name: "chat_list_ud")
   public static let code = ImageAsset(name: "code")
   public static let contactAddPlaceholder = ImageAsset(name: "contact_add_placeholder")
   public static let contactDetailsPadlock = ImageAsset(name: "contact_details_padlock")
@@ -99,10 +101,14 @@ public enum Asset {
   public static let restoreDropbox = ImageAsset(name: "restore_dropbox")
   public static let restoreIcloud = ImageAsset(name: "restore_icloud")
   public static let restoreSuccess = ImageAsset(name: "restore_success")
+  public static let scanAdd = ImageAsset(name: "scan_add")
+  public static let scanCopy = ImageAsset(name: "scan_copy")
+  public static let scanDropdown = ImageAsset(name: "scan_dropdown")
   public static let scanEmail = ImageAsset(name: "scan_email")
   public static let scanError = ImageAsset(name: "scan_error")
   public static let scanPhone = ImageAsset(name: "scan_phone")
   public static let scanQr = ImageAsset(name: "scan_qr")
+  public static let scanScan = ImageAsset(name: "scan_scan")
   public static let searchEmail = ImageAsset(name: "search_email")
   public static let searchLens = ImageAsset(name: "search_lens")
   public static let searchPhone = ImageAsset(name: "search_phone")
@@ -182,6 +188,17 @@ public final class ColorAsset {
     return color
   }()
 
+  #if os(iOS) || os(tvOS)
+  @available(iOS 11.0, tvOS 11.0, *)
+  public func color(compatibleWith traitCollection: UITraitCollection) -> Color {
+    let bundle = BundleToken.bundle
+    guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
+      fatalError("Unable to load color asset named \(name).")
+    }
+    return color
+  }
+  #endif
+
   fileprivate init(name: String) {
     self.name = name
   }
@@ -210,6 +227,7 @@ public struct ImageAsset {
   public typealias Image = UIImage
   #endif
 
+  @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
   public var image: Image {
     let bundle = BundleToken.bundle
     #if os(iOS) || os(tvOS)
@@ -225,9 +243,21 @@ public struct ImageAsset {
     }
     return result
   }
+
+  #if os(iOS) || os(tvOS)
+  @available(iOS 8.0, tvOS 9.0, *)
+  public func image(compatibleWith traitCollection: UITraitCollection) -> Image {
+    let bundle = BundleToken.bundle
+    guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
+      fatalError("Unable to load image asset named \(name).")
+    }
+    return result
+  }
+  #endif
 }
 
 public extension ImageAsset.Image {
+  @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
   @available(macOS, deprecated,
     message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
   convenience init?(asset: ImageAsset) {
diff --git a/Sources/Shared/AutoGenerated/Fonts.swift b/Sources/Shared/AutoGenerated/Fonts.swift
index 9a556412..30c2dd3f 100644
--- a/Sources/Shared/AutoGenerated/Fonts.swift
+++ b/Sources/Shared/AutoGenerated/Fonts.swift
@@ -1,7 +1,7 @@
 // swiftlint:disable all
 // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
 
-#if os(OSX)
+#if os(macOS)
   import AppKit.NSFont
 #elseif os(iOS) || os(tvOS) || os(watchOS)
   import UIKit.UIFont
@@ -51,7 +51,7 @@ public struct FontConvertible {
   public let family: String
   public let path: String
 
-  #if os(OSX)
+  #if os(macOS)
   public typealias Font = NSFont
   #elseif os(iOS) || os(tvOS) || os(watchOS)
   public typealias Font = UIFont
@@ -82,7 +82,7 @@ public extension FontConvertible.Font {
     if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
       font.register()
     }
-    #elseif os(OSX)
+    #elseif os(macOS)
     if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
       font.register()
     }
diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift
index 722a7d52..755693f3 100644
--- a/Sources/Shared/AutoGenerated/Strings.swift
+++ b/Sources/Shared/AutoGenerated/Strings.swift
@@ -172,6 +172,61 @@ public enum Localized {
     }
   }
 
+  public enum AccountRestore {
+    /// Account restore
+    public static let header = Localized.tr("Localizable", "accountRestore.header")
+    public enum Found {
+      /// Cancel
+      public static let cancel = Localized.tr("Localizable", "accountRestore.found.cancel")
+      /// BACKUP DATE
+      public static let date = Localized.tr("Localizable", "accountRestore.found.date")
+      /// Next
+      public static let next = Localized.tr("Localizable", "accountRestore.found.next")
+      /// Restore account
+      public static let restore = Localized.tr("Localizable", "accountRestore.found.restore")
+      /// FILE SIZE
+      public static let size = Localized.tr("Localizable", "accountRestore.found.size")
+      /// Restore your contacts from the following backup.
+      public static let subtitle = Localized.tr("Localizable", "accountRestore.found.subtitle")
+      /// Backup found
+      public static let title = Localized.tr("Localizable", "accountRestore.found.title")
+    }
+    public enum List {
+      /// Cancel
+      public static let cancel = Localized.tr("Localizable", "accountRestore.list.cancel")
+      /// Restore your account from a previous backup. You’ll be able to have access to all your contacts.
+      public static let firstSubtitle = Localized.tr("Localizable", "accountRestore.list.firstSubtitle")
+      /// Select the cloud storage service you previously used to create a backup.
+      public static let secondSubtitle = Localized.tr("Localizable", "accountRestore.list.secondSubtitle")
+      /// Restore your #account#.
+      public static let title = Localized.tr("Localizable", "accountRestore.list.title")
+    }
+    public enum NotFound {
+      /// Go back
+      public static let back = Localized.tr("Localizable", "accountRestore.notFound.back")
+      /// No account backup was found in %@
+      public static func subtitle(_ p1: Any) -> String {
+        return Localized.tr("Localizable", "accountRestore.notFound.subtitle", String(describing: p1))
+      }
+      /// Backup not found
+      public static let title = Localized.tr("Localizable", "accountRestore.notFound.title")
+    }
+    public enum Success {
+      /// You now have access to all your contacts.
+      public static let subtitle = Localized.tr("Localizable", "accountRestore.success.subtitle")
+      /// Your #account# has been successfully #restored#.
+      public static let title = Localized.tr("Localizable", "accountRestore.success.title")
+    }
+    public enum Warning {
+      /// I understand
+      public static let action = Localized.tr("Localizable", "accountRestore.warning.action")
+      /// xx messenger account can only run on a single device at a time. Using the same account on multiple devices may permanently damage your account and make it impossible to converse with your contacts
+      public static let subtitle = Localized.tr("Localizable", "accountRestore.warning.subtitle")
+      /// Warning
+      public static let title = Localized.tr("Localizable", "accountRestore.warning.title")
+    }
+  }
+
   public enum Backup {
     /// Dropbox
     public static let dropbox = Localized.tr("Localizable", "backup.dropbox")
@@ -267,13 +322,15 @@ public enum Localized {
       public static let title = Localized.tr("Localizable", "chat.clear.title")
     }
     public enum E2e {
-      /// You and %@ now have a #quantum-secure#, completely private channel for messaging.\n#Say hello#!
+      /// You and %@ now have a #quantum-secure#, completely private channel for messaging.
+      /// #Say hello#!
       public static func placeholder(_ p1: Any) -> String {
         return Localized.tr("Localizable", "chat.e2e.placeholder", String(describing: p1))
       }
     }
     public enum Menu {
-      /// Delete\nAll
+      /// Delete
+      /// All
       public static let deleteAll = Localized.tr("Localizable", "chat.menu.deleteAll")
     }
     public enum RetrySheet {
@@ -284,6 +341,12 @@ public enum Localized {
       /// Try again
       public static let retry = Localized.tr("Localizable", "chat.retrySheet.retry")
     }
+    public enum RoundDrawer {
+      /// OK
+      public static let action = Localized.tr("Localizable", "chat.roundDrawer.action")
+      /// The mix for this message will be available shortly, please check again later.
+      public static let title = Localized.tr("Localizable", "chat.roundDrawer.title")
+    }
     public enum SheetMenu {
       /// Clear chat
       public static let clear = Localized.tr("Localizable", "chat.sheetMenu.clear")
@@ -322,7 +385,9 @@ public enum Localized {
     public enum DeleteAll {
       /// Delete All Chats
       public static let delete = Localized.tr("Localizable", "chatList.deleteAll.delete")
-      /// All chats will be deleted from this phone. However, your contacts and their copies of your chats will remain unchanged. Encrypted copies may remain on the decentralized network for up to three weeks.\n\nThis will only delete chats locally—they can remain on the network (only decryptable by you) for up to three weeks, and they will also remain on the recipient(s) device(s).
+      /// All chats will be deleted from this phone. However, your contacts and their copies of your chats will remain unchanged. Encrypted copies may remain on the decentralized network for up to three weeks.
+      /// 
+      /// This will only delete chats locally—they can remain on the network (only decryptable by you) for up to three weeks, and they will also remain on the recipient(s) device(s).
       public static let subtitle = Localized.tr("Localizable", "chatList.deleteAll.subtitle")
       /// Delete All Chats?
       public static let title = Localized.tr("Localizable", "chatList.deleteAll.title")
@@ -555,6 +620,25 @@ public enum Localized {
     }
   }
 
+  public enum Launch {
+    public enum Version {
+      /// Failed checking app version
+      public static let failed = Localized.tr("Localizable", "launch.version.failed")
+      public enum Recommended {
+        /// Not now
+        public static let negative = Localized.tr("Localizable", "launch.version.recommended.negative")
+        /// Update
+        public static let positive = Localized.tr("Localizable", "launch.version.recommended.positive")
+        /// There is a new version available that enhance the current performance and usability.
+        public static let title = Localized.tr("Localizable", "launch.version.recommended.title")
+      }
+      public enum Required {
+        /// Okay
+        public static let positive = Localized.tr("Localizable", "launch.version.required.positive")
+      }
+    }
+  }
+
   public enum Menu {
     /// Build %@
     public static func build(_ p1: Any) -> String {
@@ -712,7 +796,9 @@ public enum Localized {
       public static let skip = Localized.tr("Localizable", "onboarding.welcome.skip")
       /// Would you like to register an email or phone number to help other users find your account? If not, you can still be found by your username, or completely off the grid using QR codes.
       public static let subtitle = Localized.tr("Localizable", "onboarding.welcome.subtitle")
-      /// %@,\nwelcome to\n#xx network#
+      /// %@,
+      /// welcome to
+      /// #xx network#
       public static func title(_ p1: Any) -> String {
         return Localized.tr("Localizable", "onboarding.welcome.title", String(describing: p1))
       }
@@ -733,7 +819,8 @@ public enum Localized {
       public static func resend(_ p1: Any) -> String {
         return Localized.tr("Localizable", "profile.code.resend", String(describing: p1))
       }
-      /// Enter the code we just sent to\n%@
+      /// Enter the code we just sent to
+      /// %@
       public static func subtitle(_ p1: Any) -> String {
         return Localized.tr("Localizable", "profile.code.subtitle", String(describing: p1))
       }
@@ -899,77 +986,33 @@ public enum Localized {
     }
   }
 
-  public enum Restore {
-    /// Account restore
-    public static let header = Localized.tr("Localizable", "restore.header")
-    public enum Found {
-      /// Cancel
-      public static let cancel = Localized.tr("Localizable", "restore.found.cancel")
-      /// BACKUP DATE
-      public static let date = Localized.tr("Localizable", "restore.found.date")
-      /// Next
-      public static let next = Localized.tr("Localizable", "restore.found.next")
-      /// Restore account
-      public static let restore = Localized.tr("Localizable", "restore.found.restore")
-      /// FILE SIZE
-      public static let size = Localized.tr("Localizable", "restore.found.size")
-      /// Restore your contacts from the following backup.
-      public static let subtitle = Localized.tr("Localizable", "restore.found.subtitle")
-      /// Backup found
-      public static let title = Localized.tr("Localizable", "restore.found.title")
-    }
-    public enum List {
-      /// Cancel
-      public static let cancel = Localized.tr("Localizable", "restore.list.cancel")
-      /// Restore your account from a previous backup. You’ll be able to have access to all your contacts.
-      public static let firstSubtitle = Localized.tr("Localizable", "restore.list.firstSubtitle")
-      /// Select the cloud storage service you previously used to create a backup.
-      public static let secondSubtitle = Localized.tr("Localizable", "restore.list.secondSubtitle")
-      /// Restore your #account#.
-      public static let title = Localized.tr("Localizable", "restore.list.title")
-    }
-    public enum NotFound {
-      /// Go back
-      public static let back = Localized.tr("Localizable", "restore.notFound.back")
-      /// No account backup was found in %@
-      public static func subtitle(_ p1: Any) -> String {
-        return Localized.tr("Localizable", "restore.notFound.subtitle", String(describing: p1))
-      }
-      /// Backup not found
-      public static let title = Localized.tr("Localizable", "restore.notFound.title")
-    }
-    public enum Success {
-      /// You now have access to all your contacts.
-      public static let subtitle = Localized.tr("Localizable", "restore.success.subtitle")
-      /// Your #account# has been successfully #restored#.
-      public static let title = Localized.tr("Localizable", "restore.success.title")
-    }
-    public enum Warning {
-      /// I understand
-      public static let action = Localized.tr("Localizable", "restore.warning.action")
-      /// xx messenger account can only run on a single device at a time. Using the same account on multiple devices may permanently damage your account and make it impossible to converse with your contacts
-      public static let subtitle = Localized.tr("Localizable", "restore.warning.subtitle")
-      /// Warning
-      public static let title = Localized.tr("Localizable", "restore.warning.title")
-    }
-  }
-
   public enum Scan {
     /// Go to contact
     public static let contact = Localized.tr("Localizable", "scan.contact")
     /// Check requests
     public static let requests = Localized.tr("Localizable", "scan.requests")
-    /// Sending as\n#%@#
+    /// Sending as
+    /// #%@#
     public static func sendingAs(_ p1: Any) -> String {
       return Localized.tr("Localizable", "scan.sendingAs", String(describing: p1))
     }
     /// Go to settings
     public static let settings = Localized.tr("Localizable", "scan.settings")
     public enum Display {
+      /// Copied!
+      public static let copied = Localized.tr("Localizable", "scan.display.copied")
+      /// Tap code to copy
+      public static let copy = Localized.tr("Localizable", "scan.display.copy")
       public enum Share {
+        /// Add
+        public static let add = Localized.tr("Localizable", "scan.display.share.add")
         /// EMAIL ADDRESS
         public static let email = Localized.tr("Localizable", "scan.display.share.email")
-        /// PHONE
+        /// ・・・・・・・・・・
+        public static let hidden = Localized.tr("Localizable", "scan.display.share.hidden")
+        /// Not added
+        public static let notAdded = Localized.tr("Localizable", "scan.display.share.notAdded")
+        /// PHONE NUMBER
         public static let phone = Localized.tr("Localizable", "scan.display.share.phone")
         /// Select what you'd like to share
         public static let title = Localized.tr("Localizable", "scan.display.share.title")
@@ -978,7 +1021,8 @@ public enum Localized {
     public enum Error {
       /// Camera needs permission to be used
       public static let denied = Localized.tr("Localizable", "scan.error.denied")
-      /// You've already added \n#%@#
+      /// You've already added 
+      /// #%@#
       public static func friends(_ p1: Any) -> String {
         return Localized.tr("Localizable", "scan.error.friends", String(describing: p1))
       }
@@ -1045,6 +1089,12 @@ public enum Localized {
         /// Record logs
         public static let title = Localized.tr("Localizable", "settings.advanced.logs.title")
       }
+      public enum ShowUsername {
+        /// Allow us to show a more detailed push notification
+        public static let description = Localized.tr("Localizable", "settings.advanced.showUsername.description")
+        /// Rich notifications
+        public static let title = Localized.tr("Localizable", "settings.advanced.showUsername.title")
+      }
     }
     public enum Biometrics {
       /// Enable unlocking with your device biometrics.
@@ -1059,7 +1109,9 @@ public enum Localized {
       public static let delete = Localized.tr("Localizable", "settings.delete.delete")
       /// Your username
       public static let input = Localized.tr("Localizable", "settings.delete.input")
-      /// A deleted account cannot be recovered. The username associated with this account cannot be reused in the future.\n\nTo confirm your account deletion, type in your username.
+      /// A deleted account cannot be recovered. The username associated with this account cannot be reused in the future.
+      /// 
+      /// To confirm your account deletion, type in your username.
       public static let subtitle = Localized.tr("Localizable", "settings.delete.subtitle")
       /// Delete Account
       public static let title = Localized.tr("Localizable", "settings.delete.title")
@@ -1159,6 +1211,10 @@ public enum Localized {
       /// Search
       public static let placeholder = Localized.tr("Localizable", "shared.search.placeholder")
     }
+    public enum SnackBar {
+      /// Connecting to xx network...
+      public static let title = Localized.tr("Localizable", "shared.snackBar.title")
+    }
   }
 
   public enum Validator {
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json
new file mode 100644
index 00000000..5c2f805e
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf
new file mode 100644
index 00000000..795087fa
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_new_group.imageset/Icon.pdf
@@ -0,0 +1,193 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 0.000000 3.384644 cm
+0.141667 0.141667 0.141667 scn
+7.384813 11.999994 m
+7.384813 14.548988 9.451180 16.615356 12.000175 16.615356 c
+14.549169 16.615356 16.615538 14.548988 16.615538 11.999994 c
+16.615538 9.450999 14.549169 7.384631 12.000175 7.384631 c
+9.451180 7.384631 7.384813 9.450999 7.384813 11.999994 c
+h
+12.000175 14.769212 m
+10.470778 14.769212 9.230957 13.529390 9.230957 11.999994 c
+9.230957 10.470597 10.470778 9.230776 12.000175 9.230776 c
+13.529572 9.230776 14.769392 10.470597 14.769392 11.999994 c
+14.769392 13.529390 13.529572 14.769212 12.000175 14.769212 c
+h
+18.461683 11.999939 m
+18.753178 11.999939 19.040531 11.930912 19.300220 11.798508 c
+19.559912 11.666103 19.784555 11.474085 19.955769 11.238171 c
+20.126982 11.002256 20.239897 10.729151 20.285271 10.441208 c
+20.330643 10.153265 20.307186 9.858670 20.216820 9.581535 c
+20.126451 9.304401 19.971743 9.052606 19.765354 8.846758 c
+19.558966 8.640909 19.306765 8.486859 19.029394 8.397219 c
+18.752024 8.307577 18.457369 8.284892 18.169546 8.331019 c
+17.881723 8.377148 17.608883 8.490733 17.373419 8.662563 c
+16.285152 7.171278 l
+16.286116 7.170577 l
+16.756842 6.827298 17.302124 6.600278 17.877394 6.508082 c
+17.940798 6.497921 18.004368 6.489429 18.068037 6.482602 c
+18.582455 6.427444 19.103485 6.480942 19.597141 6.640484 c
+20.151897 6.819772 20.656313 7.127878 21.069103 7.539588 c
+21.481892 7.951297 21.791319 8.454904 21.972061 9.009190 c
+22.152802 9.563475 22.199718 10.152681 22.108969 10.728584 c
+22.018219 11.304486 21.792381 11.850714 21.449944 12.322557 c
+21.145224 12.742432 20.755880 13.092785 20.307827 13.351466 c
+20.252371 13.383484 20.196014 13.414097 20.138809 13.443264 c
+19.619413 13.708080 19.044691 13.846084 18.461683 13.846084 c
+18.461683 11.999939 l
+h
+22.152224 0.000051 m
+22.152224 0.484699 22.056761 0.964602 21.871296 1.412360 c
+21.685827 1.860116 21.413988 2.266958 21.071287 2.609656 c
+20.728590 2.952355 20.321747 3.224197 19.873991 3.409665 c
+19.426233 3.595132 18.946331 3.690590 18.461683 3.690590 c
+18.461683 5.538486 l
+19.091291 5.538486 19.715565 5.431134 20.307827 5.221738 c
+20.399738 5.189242 20.490879 5.154289 20.581150 5.116899 c
+21.253103 4.838566 21.863657 4.430608 22.377949 3.916316 c
+22.892241 3.402025 23.300196 2.791472 23.578529 2.119518 c
+23.615919 2.029247 23.650875 1.938107 23.683371 1.846196 c
+23.892767 1.253934 24.000118 0.629662 24.000118 0.000051 c
+22.152224 0.000051 l
+h
+16.615538 0.000051 m
+18.461683 0.000051 l
+18.461683 3.568644 15.568768 6.461558 12.000175 6.461558 c
+8.431583 6.461558 5.538668 3.568644 5.538668 0.000051 c
+7.384813 0.000051 l
+7.384813 2.549046 9.451180 4.615414 12.000175 4.615414 c
+14.549170 4.615414 16.615538 2.549046 16.615538 0.000051 c
+h
+5.538435 12.000669 m
+5.246940 12.000669 4.959587 11.931643 4.699897 11.799238 c
+4.440207 11.666834 4.215563 11.474816 4.044349 11.238902 c
+3.873136 11.002987 3.760221 10.729881 3.714847 10.441939 c
+3.669474 10.153996 3.692931 9.859402 3.783298 9.582268 c
+3.873666 9.305134 4.028375 9.053337 4.234764 8.847488 c
+4.441152 8.641641 4.693353 8.487591 4.970723 8.397950 c
+5.248093 8.308309 5.542748 8.285624 5.830571 8.331751 c
+6.118393 8.377879 6.391234 8.491465 6.626700 8.663296 c
+7.714964 7.172009 l
+7.714002 7.171309 l
+7.243277 6.828030 6.697994 6.601009 6.122724 6.508814 c
+6.059320 6.498652 5.995750 6.490161 5.932080 6.483334 c
+5.417663 6.428175 4.896632 6.481673 4.402976 6.641215 c
+3.848219 6.820502 3.343803 7.128610 2.931013 7.540319 c
+2.518225 7.952028 2.208798 8.455635 2.028056 9.009921 c
+1.847316 9.564205 1.800400 10.153413 1.891150 10.729315 c
+1.981899 11.305218 2.207736 11.851444 2.550173 12.323289 c
+2.854893 12.743163 3.244237 13.093515 3.692290 13.352198 c
+3.747746 13.384215 3.804103 13.414828 3.861309 13.443995 c
+4.380703 13.708812 4.955427 13.846815 5.538435 13.846815 c
+5.538435 12.000669 l
+h
+2.128822 1.413092 m
+1.943356 0.965334 1.847895 0.485430 1.847895 0.000782 c
+0.000000 0.000782 l
+0.000000 0.630393 0.107352 1.254665 0.316748 1.846928 c
+0.349244 1.938839 0.384197 2.029980 0.421588 2.120250 c
+0.699921 2.792204 1.107878 3.402757 1.622169 3.917048 c
+2.136461 4.431339 2.747014 4.839297 3.418968 5.117630 c
+3.509238 5.155020 3.600379 5.189974 3.692290 5.222469 c
+4.284553 5.431867 4.908826 5.539218 5.538435 5.539218 c
+5.538435 3.691321 l
+5.053787 3.691321 4.573884 3.595862 4.126127 3.410397 c
+3.678370 3.224929 3.271528 2.953086 2.928830 2.610388 c
+2.586131 2.267690 2.314289 1.860847 2.128822 1.413092 c
+h
+f*
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 13.000000 14.000000 cm
+1.000000 1.000000 1.000000 scn
+10.000000 8.000000 m
+10.000000 0.000000 l
+3.500000 0.000000 l
+3.500000 2.000000 l
+0.000000 2.000000 l
+0.000000 8.000000 l
+10.000000 8.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 15.000000 15.000000 cm
+0.141667 0.141667 0.141667 scn
+5.000000 0.000000 m
+3.000000 0.000000 l
+3.000000 3.000000 l
+0.000000 3.000000 l
+0.000000 5.000000 l
+3.000000 5.000000 l
+3.000000 8.000000 l
+5.000000 8.000000 l
+5.000000 5.000000 l
+8.000000 5.000000 l
+8.000000 3.000000 l
+5.000000 3.000000 l
+5.000000 0.000000 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  5120
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000005210 00000 n
+0000005233 00000 n
+0000005406 00000 n
+0000005480 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+5539
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json
new file mode 100644
index 00000000..5c2f805e
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf
new file mode 100644
index 00000000..840f4655
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsChatList/chat_list_ud.imageset/Icon.pdf
@@ -0,0 +1,199 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 5.000000 1.000000 cm
+0.141667 0.141667 0.141667 scn
+13.000000 3.000000 m
+1.000000 3.000000 l
+1.000000 1.000000 l
+13.000000 1.000000 l
+13.000000 3.000000 l
+h
+1.000000 3.000000 m
+1.000000 19.000000 l
+-1.000000 19.000000 l
+-1.000000 3.000000 l
+1.000000 3.000000 l
+h
+1.000000 19.000000 m
+8.000000 19.000000 l
+8.000000 21.000000 l
+1.000000 21.000000 l
+1.000000 19.000000 l
+h
+13.000000 12.500000 m
+13.000000 3.000000 l
+15.000000 3.000000 l
+15.000000 12.500000 l
+13.000000 12.500000 l
+h
+1.000000 3.000000 m
+1.000000 3.000000 l
+-1.000000 3.000000 l
+-1.000000 1.895430 -0.104569 1.000000 1.000000 1.000000 c
+1.000000 3.000000 l
+h
+13.000000 1.000000 m
+14.104569 1.000000 15.000000 1.895430 15.000000 3.000000 c
+13.000000 3.000000 l
+13.000000 3.000000 l
+13.000000 1.000000 l
+h
+1.000000 19.000000 m
+1.000000 19.000000 l
+1.000000 21.000000 l
+-0.104570 21.000000 -1.000000 20.104568 -1.000000 19.000000 c
+1.000000 19.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 15.000000 15.000000 cm
+0.141667 0.141667 0.141667 scn
+5.000000 0.000000 m
+3.000000 0.000000 l
+3.000000 3.000000 l
+0.000000 3.000000 l
+0.000000 5.000000 l
+3.000000 5.000000 l
+3.000000 8.000000 l
+5.000000 8.000000 l
+5.000000 5.000000 l
+8.000000 5.000000 l
+8.000000 3.000000 l
+5.000000 3.000000 l
+5.000000 0.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 9.000000 6.000000 cm
+0.141667 0.141667 0.141667 scn
+1.000000 1.000000 m
+1.000000 2.104569 1.895430 3.000000 3.000000 3.000000 c
+3.000000 5.000000 l
+0.790861 5.000000 -1.000000 3.209139 -1.000000 1.000000 c
+1.000000 1.000000 l
+h
+3.000000 3.000000 m
+4.104569 3.000000 5.000000 2.104569 5.000000 1.000000 c
+7.000000 1.000000 l
+7.000000 3.209139 5.209139 5.000000 3.000000 5.000000 c
+3.000000 3.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 10.000000 10.000000 cm
+0.141667 0.141667 0.141667 scn
+3.000000 4.000000 m
+3.000000 3.447715 2.552285 3.000000 2.000000 3.000000 c
+2.000000 1.000000 l
+3.656854 1.000000 5.000000 2.343146 5.000000 4.000000 c
+3.000000 4.000000 l
+h
+2.000000 3.000000 m
+1.447715 3.000000 1.000000 3.447715 1.000000 4.000000 c
+-1.000000 4.000000 l
+-1.000000 2.343146 0.343146 1.000000 2.000000 1.000000 c
+2.000000 3.000000 l
+h
+1.000000 4.000000 m
+1.000000 4.552285 1.447715 5.000000 2.000000 5.000000 c
+2.000000 7.000000 l
+0.343146 7.000000 -1.000000 5.656854 -1.000000 4.000000 c
+1.000000 4.000000 l
+h
+2.000000 5.000000 m
+2.552285 5.000000 3.000000 4.552285 3.000000 4.000000 c
+5.000000 4.000000 l
+5.000000 5.656854 3.656854 7.000000 2.000000 7.000000 c
+2.000000 5.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 3.000000 12.000000 cm
+0.141667 0.141667 0.141667 scn
+0.000000 2.000000 m
+4.000000 2.000000 l
+4.000000 4.000000 l
+0.000000 4.000000 l
+0.000000 2.000000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 3.000000 6.000000 cm
+0.141667 0.141667 0.141667 scn
+0.000000 2.000000 m
+4.000000 2.000000 l
+4.000000 4.000000 l
+0.000000 4.000000 l
+0.000000 2.000000 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  2993
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000003083 00000 n
+0000003106 00000 n
+0000003279 00000 n
+0000003353 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+3412
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json
new file mode 100644
index 00000000..5c2f805e
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf
new file mode 100644
index 00000000..16216a0b
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_add.imageset/Icon.pdf
@@ -0,0 +1,79 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 4.166504 4.166992 cm
+0.000000 0.827451 0.929412 scn
+6.666667 4.999836 m
+6.666667 -0.000164 l
+5.000000 -0.000164 l
+5.000000 4.999836 l
+0.000000 4.999836 l
+0.000000 6.666503 l
+5.000000 6.666503 l
+5.000000 11.666504 l
+6.666667 11.666504 l
+6.666667 6.666503 l
+11.666668 6.666503 l
+11.666668 4.999836 l
+6.666667 4.999836 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  393
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 20.000000 20.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000000483 00000 n
+0000000505 00000 n
+0000000678 00000 n
+0000000752 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+811
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json
new file mode 100644
index 00000000..5c2f805e
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf
new file mode 100644
index 00000000..847db591
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_copy.imageset/Icon.pdf
@@ -0,0 +1,188 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 3.750000 0.250000 cm
+0.693500 0.711750 0.730000 scn
+6.700000 3.000000 m
+1.000000 3.000000 l
+1.000000 1.000000 l
+6.700000 1.000000 l
+6.700000 3.000000 l
+h
+1.000000 3.000000 m
+1.000000 11.500000 l
+-1.000000 11.500000 l
+-1.000000 3.000000 l
+1.000000 3.000000 l
+h
+1.000000 11.500000 m
+3.000000 11.500000 l
+3.000000 13.500000 l
+1.000000 13.500000 l
+1.000000 11.500000 l
+h
+6.700000 5.000000 m
+6.700000 3.000000 l
+8.700000 3.000000 l
+8.700000 5.000000 l
+6.700000 5.000000 l
+h
+1.000000 3.000000 m
+1.000000 3.000000 l
+-1.000000 3.000000 l
+-1.000000 1.895431 -0.104569 1.000000 1.000000 1.000000 c
+1.000000 3.000000 l
+h
+6.700000 1.000000 m
+7.804569 1.000000 8.700000 1.895431 8.700000 3.000000 c
+6.700000 3.000000 l
+6.700000 3.000000 l
+6.700000 1.000000 l
+h
+1.000000 11.500000 m
+1.000000 11.500000 l
+1.000000 13.500000 l
+-0.104569 13.500000 -1.000000 12.604569 -1.000000 11.500000 c
+1.000000 11.500000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 6.750000 3.250000 cm
+0.693500 0.711750 0.730000 scn
+5.046446 12.353554 m
+4.339340 11.646446 l
+5.046446 12.353554 l
+h
+6.700000 3.000000 m
+1.000000 3.000000 l
+1.000000 1.000000 l
+6.700000 1.000000 l
+6.700000 3.000000 l
+h
+1.000000 3.000000 m
+1.000000 11.500000 l
+-1.000000 11.500000 l
+-1.000000 3.000000 l
+1.000000 3.000000 l
+h
+1.000000 11.500000 m
+4.692893 11.500000 l
+4.692893 13.500000 l
+1.000000 13.500000 l
+1.000000 11.500000 l
+h
+6.700000 9.492893 m
+6.700000 3.000001 l
+8.700000 3.000001 l
+8.700000 9.492893 l
+6.700000 9.492893 l
+h
+4.339340 11.646446 m
+6.846447 9.139339 l
+8.260660 10.553554 l
+5.753553 13.060660 l
+4.339340 11.646446 l
+h
+8.700000 9.492893 m
+8.700000 9.890718 8.541965 10.272249 8.260660 10.553554 c
+6.846447 9.139339 l
+6.752678 9.233109 6.700000 9.360285 6.700000 9.492893 c
+8.700000 9.492893 l
+h
+4.692893 11.500000 m
+4.560285 11.500000 4.433108 11.552678 4.339340 11.646446 c
+5.753553 13.060660 l
+5.472249 13.341965 5.090717 13.500000 4.692893 13.500000 c
+4.692893 11.500000 l
+h
+1.000000 3.000000 m
+1.000000 3.000000 l
+-1.000000 3.000000 l
+-1.000000 1.895431 -0.104569 1.000000 1.000000 1.000000 c
+1.000000 3.000000 l
+h
+6.700000 1.000000 m
+7.804570 1.000000 8.700000 1.895432 8.700000 3.000001 c
+6.700000 3.000001 l
+6.700000 3.000000 l
+6.700000 1.000000 l
+h
+1.000000 11.500000 m
+1.000000 11.500000 l
+1.000000 13.500000 l
+-0.104569 13.500000 -1.000000 12.604569 -1.000000 11.500000 c
+1.000000 11.500000 l
+h
+f
+n
+Q
+q
+1.000000 0.000000 -0.000000 1.000000 11.299805 12.600098 cm
+0.693500 0.711750 0.730000 scn
+0.000000 -0.000097 m
+0.000000 3.149902 l
+3.150000 -0.000097 l
+0.000000 -0.000097 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  2624
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 18.000000 18.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000002714 00000 n
+0000002737 00000 n
+0000002910 00000 n
+0000002984 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+3043
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json
new file mode 100644
index 00000000..5c2f805e
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf
new file mode 100644
index 00000000..8a8f5358
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_dropdown.imageset/Icon.pdf
@@ -0,0 +1,73 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 5.990234 8.287109 cm
+0.141667 0.141667 0.141667 scn
+6.010000 -0.000195 m
+12.020000 6.009805 l
+10.607000 7.424805 l
+6.010000 2.824805 l
+1.414000 7.424805 l
+0.000000 6.010805 l
+6.010000 -0.000195 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  271
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000000361 00000 n
+0000000383 00000 n
+0000000556 00000 n
+0000000630 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+689
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json
index 5c2f805e..d9a97df8 100644
--- a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_qr.imageset/Contents.json
@@ -10,6 +10,7 @@
     "version" : 1
   },
   "properties" : {
-    "preserves-vector-representation" : true
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
   }
 }
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json
new file mode 100644
index 00000000..d9a97df8
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "Icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}
diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf
new file mode 100644
index 00000000..2a16f205
--- /dev/null
+++ b/Sources/Shared/Resources/Assets.xcassets/AssetsScan/scan_scan.imageset/Icon.pdf
@@ -0,0 +1,111 @@
+%PDF-1.7
+
+1 0 obj
+  << >>
+endobj
+
+2 0 obj
+  << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 3.000000 3.750000 cm
+1.000000 1.000000 1.000000 scn
+0.875000 13.750000 m
+0.875000 15.127285 1.997715 16.250000 3.375000 16.250000 c
+5.625000 16.250000 l
+5.625000 14.250000 l
+3.375000 14.250000 l
+3.102285 14.250000 2.875000 14.022715 2.875000 13.750000 c
+2.875000 11.500000 l
+0.875000 11.500000 l
+0.875000 13.750000 l
+h
+14.625000 14.250000 m
+12.375000 14.250000 l
+12.375000 16.250000 l
+14.625000 16.250000 l
+16.002285 16.250000 17.125000 15.127285 17.125000 13.750000 c
+17.125000 11.500000 l
+15.125000 11.500000 l
+15.125000 13.750000 l
+15.125000 14.022715 14.897716 14.250000 14.625000 14.250000 c
+h
+2.875000 2.500000 m
+2.875000 4.750000 l
+0.875000 4.750000 l
+0.875000 2.500000 l
+0.875000 1.122715 1.997715 0.000000 3.375000 0.000000 c
+5.625000 0.000000 l
+5.625000 2.000000 l
+3.375000 2.000000 l
+3.102285 2.000000 2.875000 2.227284 2.875000 2.500000 c
+h
+17.125000 4.750000 m
+17.125000 2.500000 l
+17.125000 1.122715 16.002285 0.000000 14.625000 0.000000 c
+12.375000 0.000000 l
+12.375000 2.000000 l
+14.625000 2.000000 l
+14.897716 2.000000 15.125000 2.227284 15.125000 2.500000 c
+15.125000 4.750000 l
+17.125000 4.750000 l
+h
+0.000000 7.125000 m
+18.000000 7.125000 l
+18.000000 9.125000 l
+0.000000 9.125000 l
+0.000000 7.125000 l
+h
+f*
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+  1298
+endobj
+
+4 0 obj
+  << /Annots []
+     /Type /Page
+     /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+     /Resources 1 0 R
+     /Contents 2 0 R
+     /Parent 5 0 R
+  >>
+endobj
+
+5 0 obj
+  << /Kids [ 4 0 R ]
+     /Count 1
+     /Type /Pages
+  >>
+endobj
+
+6 0 obj
+  << /Pages 5 0 R
+     /Type /Catalog
+  >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001388 00000 n
+0000001411 00000 n
+0000001584 00000 n
+0000001658 00000 n
+trailer
+<< /ID [ (some) (id) ]
+   /Root 6 0 R
+   /Size 7
+>>
+startxref
+1717
+%%EOF
\ No newline at end of file
diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings
index 58fb6862..774e9759 100644
--- a/Sources/Shared/Resources/en.lproj/Localizable.strings
+++ b/Sources/Shared/Resources/en.lproj/Localizable.strings
@@ -89,6 +89,11 @@
 
 // ChatFeature
 
+"chat.roundDrawer.title"
+= "The mix for this message will be available shortly, please check again later.";
+"chat.roundDrawer.action"
+= "OK";
+
 "chat.cancel"
 = "Cancel";
 "chat.bubbleMenu.copy"
@@ -167,13 +172,23 @@
 = "Place QR code inside frame to scan";
 "scan.status.success"
 = "Success";
+"scan.display.copied"
+= "Copied!";
 
 "scan.display.share.title"
 = "Select what you'd like to share";
 "scan.display.share.email"
 = "EMAIL ADDRESS";
 "scan.display.share.phone"
-= "PHONE";
+= "PHONE NUMBER";
+"scan.display.share.add"
+= "Add";
+"scan.display.share.notAdded"
+= "Not added";
+"scan.display.share.hidden"
+= "・・・・・・・・・・";
+"scan.display.copy"
+= "Tap code to copy";
 "scan.sendingAs"
 = "Sending as\n#%@#";
 "scan.segmentedControl.left"
@@ -377,6 +392,82 @@
 "requests.drawer.group.success.later"
 = "Later";
 
+"requests.drawer.single.title"
+= "REQUEST FROM";
+"requests.drawer.single.email"
+= "EMAIL ADDRESS";
+"requests.drawer.single.phone"
+= "PHONE NUMBER";
+"requests.drawer.single.nickname"
+= "Edit your new contact’s nickname.";
+"requests.drawer.single.accept"
+= "Accept and Save";
+"requests.drawer.single.hide"
+= "Hide Request";
+
+"requests.drawer.group.title"
+= "GROUP CHAT REQUEST";
+"requests.drawer.group.accept"
+= "Accept";
+"requests.drawer.group.hide"
+= "Hide Request";
+
+"requests.drawer.single.success.title"
+= "NEW CONNECTION";
+"requests.drawer.single.success.subtitle"
+= "Is now a connection, would you like to send a message?";
+"requests.drawer.single.success.send"
+= "Send a Message";
+"requests.drawer.single.success.later"
+= "Later";
+
+"requests.drawer.group.success.title"
+= "ACCEPTED";
+"requests.drawer.group.success.subtitle"
+= "You are now part of the group chat. Would you like to check it out?";
+"requests.drawer.group.success.send"
+= "Go to Chat";
+"requests.drawer.group.success.later"
+= "Later";
+
+"requests.drawer.single.title"
+= "REQUEST FROM";
+"requests.drawer.single.email"
+= "EMAIL ADDRESS";
+"requests.drawer.single.phone"
+= "PHONE NUMBER";
+"requests.drawer.single.nickname"
+= "Edit your new contact’s nickname.";
+"requests.drawer.single.accept"
+= "Accept and Save";
+"requests.drawer.single.hide"
+= "Hide Request";
+
+"requests.drawer.group.title"
+= "GROUP CHAT REQUEST";
+"requests.drawer.group.accept"
+= "Accept";
+"requests.drawer.group.hide"
+= "Hide Request";
+
+"requests.drawer.single.success.title"
+= "NEW CONNECTION";
+"requests.drawer.single.success.subtitle"
+= "Is now a connection, would you like to send a message?";
+"requests.drawer.single.success.send"
+= "Send a Message";
+"requests.drawer.single.success.later"
+= "Later";
+
+"requests.drawer.group.success.title"
+= "ACCEPTED";
+"requests.drawer.group.success.subtitle"
+= "You are now part of the group chat. Would you like to check it out?";
+"requests.drawer.group.success.send"
+= "Go to Chat";
+"requests.drawer.group.success.later"
+= "Later";
+
 // ProfileFeature
 
 "profile.email.title"
@@ -481,6 +572,11 @@
 "settings.delete"
 = "Delete account";
 
+"settings.advanced.showUsername.title"
+= "Rich notifications";
+"settings.advanced.showUsername.description"
+= "Allow us to show a more detailed push notification";
+
 "settings.drawer.title"
 = "Do you want to open %@?";
 "settings.drawer.subtitle"
@@ -717,47 +813,47 @@
 "onboarding.welcome.info.subtitle"
 = "Registration is completely optional. When registering an email or phone number, they will be evaluated by twilio, a 3rd party partner. Afterwards, salted hashes will be registered in #User Discovery# to allow other uses to search for you using the registered data completely privately.";
 
-"restore.warning.title"
+"accountRestore.warning.title"
 = "Warning";
-"restore.warning.subtitle"
+"accountRestore.warning.subtitle"
 = "xx messenger account can only run on a single device at a time. Using the same account on multiple devices may permanently damage your account and make it impossible to converse with your contacts";
-"restore.warning.action"
+"accountRestore.warning.action"
 = "I understand";
 
-"restore.list.title"
+"accountRestore.list.title"
 = "Restore your #account#.";
-"restore.list.firstSubtitle"
+"accountRestore.list.firstSubtitle"
 = "Restore your account from a previous backup. You’ll be able to have access to all your contacts.";
-"restore.list.secondSubtitle"
+"accountRestore.list.secondSubtitle"
 = "Select the cloud storage service you previously used to create a backup.";
-"restore.list.cancel"
+"accountRestore.list.cancel"
 = "Cancel";
 
-"restore.header"
+"accountRestore.header"
 = "Account restore";
-"restore.found.title"
+"accountRestore.found.title"
 = "Backup found";
-"restore.found.subtitle"
+"accountRestore.found.subtitle"
 = "Restore your contacts from the following backup.";
-"restore.found.date"
+"accountRestore.found.date"
 = "BACKUP DATE";
-"restore.found.size"
+"accountRestore.found.size"
 = "FILE SIZE";
-"restore.found.restore"
+"accountRestore.found.restore"
 = "Restore account";
-"restore.found.cancel"
+"accountRestore.found.cancel"
 = "Cancel";
-"restore.found.next"
+"accountRestore.found.next"
 = "Next";
-"restore.notFound.title"
+"accountRestore.notFound.title"
 = "Backup not found";
-"restore.notFound.subtitle"
+"accountRestore.notFound.subtitle"
 = "No account backup was found in %@";
-"restore.notFound.back"
+"accountRestore.notFound.back"
 = "Go back";
-"restore.success.title"
+"accountRestore.success.title"
 = "Your #account# has been successfully #restored#.";
-"restore.success.subtitle"
+"accountRestore.success.subtitle"
 = "You now have access to all your contacts.";
 
 // Shared
@@ -772,6 +868,8 @@
 = "Resend";
 "shared.done"
 = "Done";
+"shared.snackBar.title"
+= "Connecting to xx network...";
 
 // HUD
 
@@ -861,6 +959,19 @@
 "contactSearch.nicknameDrawer.save"
 = "Save";
 
+// LaunchFeature
+
+"launch.version.failed"
+= "Failed checking app version";
+"launch.version.required.positive"
+= "Okay";
+"launch.version.recommended.title"
+= "There is a new version available that enhance the current performance and usability.";
+"launch.version.recommended.positive"
+= "Update";
+"launch.version.recommended.negative"
+= "Not now";
+
 // Countries
 
 "countries.title"
diff --git a/Sources/Shared/Views/AvatarView.swift b/Sources/Shared/Views/AvatarView.swift
index a8fcfc1f..93a4789f 100644
--- a/Sources/Shared/Views/AvatarView.swift
+++ b/Sources/Shared/Views/AvatarView.swift
@@ -17,12 +17,13 @@ public final class AvatarView: UIView {
         layer.masksToBounds = true
         backgroundColor = Asset.brandPrimary.color
 
+        iconImageView.contentMode = .center
         imageView.contentMode = .scaleAspectFill
         monogramLabel.textColor = Asset.neutralWhite.color
 
-        addSubview(imageView)
-        addSubview(iconImageView)
         addSubview(monogramLabel)
+        addSubview(iconImageView)
+        addSubview(imageView)
 
         imageView.snp.makeConstraints {
             $0.edges.equalToSuperview()
diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift
index f3760e87..2e87cfbf 100644
--- a/Sources/Shared/Views/SearchComponent.swift
+++ b/Sources/Shared/Views/SearchComponent.swift
@@ -21,10 +21,14 @@ public final class SearchComponent: UIView {
         }
     }
 
-    private var isEditing = false
+    public var isEditingPublisher: AnyPublisher<Bool, Never> {
+        isEditingSubject.eraseToAnyPublisher()
+    }
+
     private var cancellables = Set<AnyCancellable>()
     private var rightSubject = PassthroughSubject<Void, Never>()
     private var textSubject = PassthroughSubject<String, Never>()
+    private var isEditingSubject = CurrentValueSubject<Bool, Never>(false)
 
     public init() {
         super.init(frame: .zero)
@@ -74,7 +78,7 @@ public final class SearchComponent: UIView {
         inputField.text = nil
         textSubject.send("")
         inputField.endEditing(true)
-        isEditing = false
+        isEditingSubject.send(false)
     }
 
     private func setup() {
@@ -110,7 +114,7 @@ public final class SearchComponent: UIView {
 
         rightButton.publisher(for: .touchUpInside)
             .sink { [weak rightSubject, self] in
-                if isEditing {
+                if isEditingSubject.value == true {
                     abortEditing()
                 } else {
                     rightSubject?.send()
@@ -127,31 +131,31 @@ public final class SearchComponent: UIView {
     }
 
     private func setupConstraints() {
-        containerView.snp.makeConstraints { make in
-            make.top.equalToSuperview()
-            make.left.equalToSuperview()
-            make.right.equalToSuperview()
-            make.bottom.equalToSuperview()
-            make.height.equalTo(50)
+        containerView.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.left.equalToSuperview()
+            $0.right.equalToSuperview()
+            $0.bottom.equalToSuperview()
+            $0.height.equalTo(50)
         }
 
-        leftImageView.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(10)
-            make.left.equalToSuperview().offset(13)
-            make.bottom.equalToSuperview().offset(-10)
-            make.height.equalTo(leftImageView.snp.width)
+        leftImageView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(10)
+            $0.left.equalToSuperview().offset(13)
+            $0.bottom.equalToSuperview().offset(-10)
+            $0.height.equalTo(leftImageView.snp.width)
         }
 
-        inputField.snp.makeConstraints { make in
-            make.top.bottom.equalToSuperview()
-            make.left.equalTo(leftImageView.snp.right).offset(20)
-            make.right.equalTo(rightButton.snp.left).offset(-32)
+        inputField.snp.makeConstraints {
+            $0.top.bottom.equalToSuperview()
+            $0.left.equalTo(leftImageView.snp.right).offset(20)
+            $0.right.equalTo(rightButton.snp.left).offset(-32)
         }
 
-        rightButton.snp.makeConstraints { make in
-            make.top.equalToSuperview()
-            make.right.equalToSuperview().offset(-13)
-            make.bottom.equalToSuperview()
+        rightButton.snp.makeConstraints {
+            $0.top.equalToSuperview()
+            $0.right.equalToSuperview().offset(-13)
+            $0.bottom.equalToSuperview()
         }
     }
 
@@ -162,12 +166,12 @@ public final class SearchComponent: UIView {
 
     public func textFieldDidBeginEditing(_ textField: UITextField) {
         rightButton.setImage(Asset.sharedCross.image, for: .normal)
-        isEditing = true
+        isEditingSubject.send(true)
     }
 
     public func textFieldDidEndEditing(_ textField: UITextField) {
         rightButton.setImage(rightImage, for: .normal)
-        isEditing = false
+        isEditingSubject.send(false)
     }
 }
 
diff --git a/Sources/Shared/Views/SnackBar.swift b/Sources/Shared/Views/SnackBar.swift
index f3e169cd..9240ccf9 100644
--- a/Sources/Shared/Views/SnackBar.swift
+++ b/Sources/Shared/Views/SnackBar.swift
@@ -1,42 +1,35 @@
 import UIKit
 
 public final class SnackBar: UIView {
-    // MARK: UI
-
-    let title = UILabel()
-    let icon = UIImageView()
-    let stack = UIStackView()
-
-    // MARK: Lifecycle
+    private let titleLabel = UILabel()
+    private let imageView = UIImageView()
+    private let stackView = UIStackView()
 
     public init() {
         super.init(frame: .zero)
-        setup()
-    }
 
-    required init?(coder: NSCoder) { nil }
+        //alpha = 0.0
+        backgroundColor = Asset.brandPrimary.color
 
-    // MARK: Private
+        imageView.contentMode = .center
+        titleLabel.text = Localized.Shared.SnackBar.title
+        titleLabel.font = Fonts.Mulish.semiBold.font(size: 13.0)
+        titleLabel.textColor = Asset.neutralWhite.color
+        imageView.image = Asset.sharedWhiteExclamation.image
 
-    private func setup() {
-        backgroundColor = Asset.brandPrimary.color
+        stackView.spacing = 14
+        stackView.addArrangedSubview(imageView)
+        stackView.addArrangedSubview(titleLabel)
+
+        addSubview(stackView)
 
-        icon.contentMode = .center
-        title.text = "Connecting to xx network..."
-        title.font = Fonts.Mulish.semiBold.font(size: 13.0)
-        title.textColor = Asset.neutralWhite.color
-        icon.image = Asset.sharedWhiteExclamation.image
-
-        stack.spacing = 14
-        stack.addArrangedSubview(icon)
-        stack.addArrangedSubview(title)
-
-        addSubview(stack)
-        stack.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(16)
-            make.left.equalToSuperview().offset(20)
-            make.right.equalToSuperview().offset(-20)
-            make.bottom.equalToSuperview().offset(-16)
+        stackView.snp.makeConstraints {
+            $0.top.equalToSuperview().offset(16)
+            $0.left.equalToSuperview().offset(20)
+            $0.right.equalToSuperview().offset(-20)
+            $0.bottom.equalToSuperview().offset(-16)
         }
     }
+
+    required init?(coder: NSCoder) { nil }
 }
-- 
GitLab