diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/LaunchFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..08a24384a1a211d4a7eb4696c6e957d4d0c8b5b1
--- /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 0000000000000000000000000000000000000000..2d6189486ea18021c361490678fd3abf55938ace
--- /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 9d3669c70425621e3f5b2ea1e87bd52f5fbe284a..f45f1115897444c23f2156a3cb4d260925d976fc 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 1c92ce4a7750684edcf92cbf933f616c89881f13..495958b9ab1e9b26981c9996f088b17b5fa1053d 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 3fa6e6adb9c619d26155f6b6a66c375031a734b4..9ffb59c7aba66820aaf2a22dbf6b6c8e041cff8c 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 ce90eb85306477465bf77307eed53d5434c6ba48..88e463326dd0fda0a358f095f60612c555c77d3b 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 ce66cf78fb427b5813a1fc9878c8bea961f9d885..66b1af7d2459202cf5772e2d22e1009df35777d1 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 b6319b9cef9b381e616ed1221d51c6c7c7fb33b7..ecb65d1f54eb3b05cda31850eb98431c6ec2d31f 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 5d87fd000b19903e982cd03e4ba4c9bfa742ab2a..b342330e9012ba1a245d39cbd26f47602c074db4 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 174bb5fa7e2f39ca9d793b81a195150e68fdd407..e248c053bc68431171a9817454d68fffb6f6a4a2 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 41526deb8afa5d814546dbcf6dfb23dcc3b18ad3..b6fd74a15936d7e89da27d03e555e5e16291af3e 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 e770bf23f89b1f7e001839fcf1dea22d7cc2389f..3f9da31df8b01008102bb6bd066a5b27a69ea326 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 75ac2ce6d22af0a05d643286d9dc6bad24c7b4a2..5f2cb1b6b3348382304af392042bae3184010302 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 375043827c258f12b894e210c77607795dc480f6..ad29c602f94972faaec2aec031838e81b45c2ca6 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 3ec0712e0b1a481a9f82dadfc7102e1e702e5cc0..d1be4b6ee2b69fb786f1d371162c4f4d8c420f1c 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 38979a1438b4e603c10682e30bcf4c9ae1dfbed3..5982ffc49253c7d5b7a21939290b11f0f48e2f20 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 a7564129d2cd2bdc5440e17888b446fd2a7d450c..88efa910ce3ef46a3b1f8012382b1287117b477d 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 594f9f88effa1a6d3f4fbb6551ab799232d20f61..20a74276710b492a845ad7b276fc79665a6a1baf 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 19e7361b3d7a536bd42e15990d76fa20646d7012..2c5a3c9f3f6c9e86735c28eb4edb64efcfd5d44f 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 552fed89103de016679cf1f1dbf3d5732e4bb6bd..0000000000000000000000000000000000000000
--- 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 20dce02844eff56a20f51c9951fa5c42e20fdbe6..d2d15a400f402ec532ee97a2407ccf88415c1c93 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 0000000000000000000000000000000000000000..c1abc94ac78f3f3e95f1058a2e8ede9021d11616
--- /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 af7f3c55d81242a6173947f5dee2d8bc96341b7d..17b744113f0ecc43eb229f62cd5f4ddd5d8da385 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 75958ab2f3d02726bc2af8464d5b9f8956d73d1c..42b7dda644aa04aa7f928ad8006d59715d442b4b 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 0000000000000000000000000000000000000000..3e159479aeaade0a5b1355d19eccf627f5d5f3b7
--- /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 a78407c0f6225acef3e5efafb8a389c6bb85b69a..ceb096f383a164fd6ead74e9819089b4e45a5807 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 fe59df03888602a5b06ccdc64dece68eb5e026f1..8eea62d2278b630efeb6afb1d12fad725cd23c18 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 0000000000000000000000000000000000000000..1be2c99cf0e9cc736f0c02acd1b9c3e54f5be688
--- /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 0000000000000000000000000000000000000000..374e184970bcad877ee7ca507369bb219df32265
--- /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 0000000000000000000000000000000000000000..4adbdeef783245dcd73a1697f8325654952ab3e2
--- /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 0000000000000000000000000000000000000000..6cc8a78df568a521afbb39b6fe69cd4197aa5e4e
--- /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 0000000000000000000000000000000000000000..817893e1a1df3d5f50cf241cfb0d5b60ababe2ae
--- /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 918cffcc651ccddb61d844ad61ce61799d1d6895..c7303c48d04db46983e2985d40745ce236d72c43 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 0000000000000000000000000000000000000000..2fe1471c5d1f7d9d967a2ca24b50a6950c4067c3
--- /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 e8771ab9a3d359a0a4b51ea5d7d2b5d928dd4548..4207fea75d83ea0ef6e56ae361cb5a172c8773c7 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 233b89525031ef534d1df7b84b8d59ac664086c6..4be4dfad3fccda964b68873b5ddcf3cc4253aeed 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 db9d5565c113dc3c3d62e78d7a54dcf2b69dc983..86271c2ebfcc146182d867aedf7dd622168807df 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 51bdf9d3365534ad96e669e99cb7230111d3a6dd..bdd6784d144195da495cfa24db7784fcdd08b999 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 5cfa63f047e404573bbcc20228d337c2c7e0cee4..93cf186a5df2963c605b2dc6a93c7ede36425468 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 c0ab7acfdef259bc1438b4a74de19c93bb1e18c1..ce843f4a7f44392a3705f8c738fd3b2cf99a09a4 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 ed5b307775eac621c5dfe9395d827e5e8f9a9766..34e0e4207c6e1f7625cec55ddf44d7946fa18e7a 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 f820cb04f57dfa4dfcc383a24081943c0e9b246a..23f9f2234250a113862d693254238c8bc8664b74 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 1235f0409f78f70e7c68f1bcd83b62c07b19f458..8eaf8a0688d3efdaab0ba1ccedad20bd4b6c7577 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 69cd094e0eae4ed5c281d5cc3058ad7e94945dec..910faf080c1b476af9c63677a25a6211f8f8a448 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 1c6690b55619b4c63a12f5025765923844fedeeb..53204708f1fdb09e27d1f48b0bbbde0a76c40a90 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 3eea889ce7d3b75dae46da9f277848ef8490ae1b..b4e98e9647d8292051d8df61679d629740c84407 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 7e0f40d9ba28d6ffac4c05f37f6aa0cbe5d6b16a..2982b295ff964a8eae68c7fd6d58d4ca37788a7c 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 a1e2c7331472bbbe6d2c64d9a2daeeeaa7bbd64d..121469499c3841d9e1921f14b7f5efdf52a8093e 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 34d656ec2d432f54ecac1693cbf8e27d05c9db30..1273a26a68e4487227c96fe953ecabe23994e450 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 0000000000000000000000000000000000000000..33c2f8dad55d29d02a7e74b3d70437fbaea4aa6f
--- /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 0000000000000000000000000000000000000000..2f446810a29e5639190477705ffd8225bb88eb37
--- /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 0000000000000000000000000000000000000000..995c9f8c6b66501798069a736594c3e4a87a5537
--- /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 0000000000000000000000000000000000000000..5e1b78a61a04e567242a2b058f7d5afa872bfb7f
--- /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 0000000000000000000000000000000000000000..571c2a527a33e8411c320cbc7b25fdec6145d399
--- /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 f80b0ae1d135c1bcf913efe8cc667a4e7d178aed..1959743eb66d6888db0d7c65e07f5cfd0e11826a 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 e086573ebef7b0f01eecd1c88178b898c3a39368..0000000000000000000000000000000000000000
--- 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 073ac9a0706dab3e97392a16752761cfb3628d40..9b39ff6dbbd1cbad8202bd8117adfb9fc606b2c3 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 d84355e2a5c90bbdbee48820a7d6a11f51613e0b..473eb593ece68fd213a4896abe9a74eb3c23d286 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 f7e3087e4bc2425f901623b088d6b414bde1d65f..0000000000000000000000000000000000000000
--- 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 da4249e349d52195df01de3ff897822b6399bb25..ca73d7c55d96709e85f076e6e06d74e6bc09a155 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 bfc03a0cf4f265157da60da3704e587c9253d755..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..9aa75041564c08334bf2f8068d118128c3a8b52f
--- /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 0000000000000000000000000000000000000000..41aced17ade52d9c4b02ec092feec29276fc21fb
--- /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 0000000000000000000000000000000000000000..51c25bd52f513816ca850930e07a4c0a72204798
--- /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 0000000000000000000000000000000000000000..045f0e898276350914d9c0d335a58997d4fbdfa1
--- /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 0000000000000000000000000000000000000000..bb0b19cd27060c2bf475ba948acf0333debc388e
--- /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 0000000000000000000000000000000000000000..c17c7e600d9c8f3ed222f3e7b8e1d6db035d85ce
--- /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 0000000000000000000000000000000000000000..6fc66797612acf41e56213ab64ffc9a7029d873e
--- /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 0000000000000000000000000000000000000000..7cd2fefefcbce0968a7d4478aa4e5cc01d771f98
--- /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 d49dba163de277007db1c95d649db917cd5a7b80..0000000000000000000000000000000000000000
--- 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 4a2c09d8b1305c9174e0280b858d9fabf7d98d23..0000000000000000000000000000000000000000
--- 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 a77cc3a3ac5bb90858ae32f0f2c3b833b4e3edef..b88932c148df5249e2c88184b3506e335596020f 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 43767aed6f40e139a002be9ee31dc96efb9b6b1f..041717b6dde907568db4872827957d487010d006 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 26d937db458bc12f164141a32c665f1ac57ee12c..2a688760bc68cd509775fa97d28a37538dc44d3a 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 94f318951e8d3f447d72a2d2188a37aca398b84f..35bdd4c04fc356e32b54a729b642514e504f8c38 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 52686480e81c655b774bb59703c30f7ef8dc3f99..e36e5d5b1dd97cb735ac84f9d1c5c56fdb19b5c3 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 f02b4ef89d5f757a4bfe19dd033ee3ba0616695a..fb1f9d440421512de2f3034c1b46b5d1fd70b2a3 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 c172703bedd8b566cdf388172abc52a6804318f4..2a1b99aa6da269007f9692b5ebb0b3b81e6ca377 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 f772340fdc0bae3b03c8bfc4deab740082813215..e373d4de40ca31bae004de3d80c85702d538b604 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 86ed4bc1f15f158c27cff79fc121f6ec6761b8e8..6a36db1801d9874f687a3eef13dabf657125f943 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 00ad9e1e70ba821cb43bae9e3cc176c53b6d180f..df4560653bbf5bd8f342148a3847ddef9f383d97 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 8656ad14c35f1445c4c8b64da15e432a32e14095..c3f51841e1726407c69f47de05f15570f01c438f 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 4c62bdba700343900ef8c3612d08e5fcd7b6411b..4bc957c6816dfc1c36b50ed5c47e46d6cf0180f5 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 5e8328284a7b2136a792471dc5fabf42e3d9ce3e..b9b35cef12d02a3a8ea88ead84692938e31ed773 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 79030c63d7eedc91d37fb23878c7c5ee8ab97cb7..acad98f7cab2ffd36c0b210737c17cd26dc21904 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 c1a1b23b402e1286a2475edf225794f736432edd..fa2b02438c0d735708b9a30ffc4a6c2228926890 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 995e7eb2255f6f0719359d92ae9d806e1686e79d..14bc4c5dc736a388a41d9c04f21a51fa49467e99 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 0000000000000000000000000000000000000000..66d911c602a67c4ad01a02002fc4f9de2b2563ac
--- /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 a21ceb854bea263ae0912541b4aff23a42b29b78..7aff4cf2c85928c1e9ef71bcebe2590b0eb1cf09 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 3b9d9a5e3a43372257b64220b4607a98d40b16bd..11faf5a55448b39530fc66a71610a8b49852dfd0 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 8270a1947e8eed7360cd114fd31e13332e012566..c23f16c1539338457198024f84216ffffdca1590 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 f750fc38c472379547b721f1ac148139b77cc619..7702558f24ae08d75c701ef4203a52fe1607574f 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 1645d25a32e6325eb902a122f2633cabdfd0cab7..aeb6791eefb9685595edd21c816026de9f2ebb0a 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 ed304656ddc9bf88a262bb01beb8c9aeb29194c9..8717024458fee4281461d03a5c05649cca3ce576 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 9eb30e134418cbaad8df3a6bd2e32562e594277d..97c146a6aa7645ef12298e8bcc2dae2edf2072e2 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 e7f96ee81c740660545ef29d402c419046a8fbb6..fb3df7c771e7b5a205b5ec610ca7780bce045f88 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 76164e8f59a5329d780a81ac348062fce2cebf66..2b2248fb83ad430ec95d9378a5d5762e8d74c046 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 9a556412b01200e3714ce73d06b3961f358a28d2..30c2dd3faf140aca2eaed5c7b30ac31eaf876dc5 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 722a7d52b37b383c03d53eb9befc5bd20bb0a0c7..755693f39ac7ba661a99e93b8d64360590ed299b 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 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /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 0000000000000000000000000000000000000000..795087fa93911997334099cced454c4b1e35cebf
--- /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 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /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 0000000000000000000000000000000000000000..840f4655381abe01930b66d33c19691714be2e08
--- /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 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /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 0000000000000000000000000000000000000000..16216a0b7b4bc311ea8abf1e543af2752bded38a
--- /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 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /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 0000000000000000000000000000000000000000..847db5913192df42797261645c76b454a29bc4a7
--- /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 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f
--- /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 0000000000000000000000000000000000000000..8a8f535835295912913921bff26af9f4e484088c
--- /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 5c2f805eb3317d819659b5d73f14b6a1b249804f..d9a97df899595ebc48148baa9cbedb9be8f763fa 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 0000000000000000000000000000000000000000..d9a97df899595ebc48148baa9cbedb9be8f763fa
--- /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 0000000000000000000000000000000000000000..2a16f205557366e051c9f577878b86ef642d60a6
--- /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 58fb6862cc40f861409a72abe30398aa6614c75e..774e9759242a913fc93e1d9b025ced9ca2b847b9 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 a8fcfc1f32e5345697136110a71d842028da7a40..93a4789f6b22df1f60d4470c05df1db69e2d9cd6 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 f3760e87ca8aa06364ee44f1cbf237260cf406b8..2e87cfbf6dceddcb50009a78b41fd2c0aa598ea5 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 f3e169cd6ae70dc9bce9fa7f0392fdefe3bfe6d6..9240ccf9537d12cf8377bcfb4e0d222442325f4c 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 }
 }