UI: update smart list

- use new design for smart list
- migrate to swiftUI

Change-Id: Iadbd93b631743640b7b7ab0a62a64333da3b0bd4
diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index f38a1ba..d463a11 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -149,7 +149,6 @@
 		1A2D18C71F29180700B2C785 /* DeviceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BF1F29180700B2C785 /* DeviceModel.swift */; };
 		1A2D18D11F29182500B2C785 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */; };
 		1A2D18DD1F29192D00B2C785 /* MessableBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18DC1F29192D00B2C785 /* MessableBubble.swift */; };
-		1A2D18EB1F29197100B2C785 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18E41F29197100B2C785 /* MessageViewModel.swift */; };
 		1A2D18ED1F2919D800B2C785 /* MeViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18EC1F2919D800B2C785 /* MeViewController.storyboard */; };
 		1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */; };
 		1A2D18FD1F292DAD00B2C785 /* SmartListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18FB1F292DAD00B2C785 /* SmartListCell.xib */; };
@@ -245,6 +244,10 @@
 		265DFB09292FD25000834B97 /* DefaultTransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265DFB08292FD25000834B97 /* DefaultTransferView.swift */; };
 		265DFB0B292FD41100834B97 /* MessageSwiftUIViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265DFB0A292FD41100834B97 /* MessageSwiftUIViews.swift */; };
 		265DFB1129302A8700834B97 /* ContactMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265DFB1029302A8700834B97 /* ContactMessageView.swift */; };
+		26625BB52BA9DF67009D2DDB /* ConversationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26625BB42BA9DF67009D2DDB /* ConversationsView.swift */; };
+		26625BB72BA9DF81009D2DDB /* ConversationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26625BB62BA9DF81009D2DDB /* ConversationsViewModel.swift */; };
+		26625BB92BA9FEFC009D2DDB /* SmartListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26625BB82BA9FEFC009D2DDB /* SmartListContentView.swift */; };
+		26625BBB2BAA0749009D2DDB /* SmartListContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26625BBA2BAA0749009D2DDB /* SmartListContainer.swift */; };
 		2662FC79246B1E1700FA7782 /* JamiSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC78246B1E1700FA7782 /* JamiSearchView.swift */; };
 		2662FC7B246B216B00FA7782 /* JamiSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC7A246B216B00FA7782 /* JamiSearchViewModel.swift */; };
 		2662FC7D246B78E800FA7782 /* IncognitoSmartListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */; };
@@ -393,12 +396,18 @@
 		26D08AB9269628F400E37574 /* RequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D08AB8269628F400E37574 /* RequestsService.swift */; };
 		26D08ABB2696293100E37574 /* RequestsAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D08ABA2696293100E37574 /* RequestsAdapterDelegate.swift */; };
 		26D08ABE2696296300E37574 /* RequestsAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 26D08ABD2696296300E37574 /* RequestsAdapter.mm */; };
+		26D87CEB2BBB4A5F0086E4AA /* AccountsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D87CEA2BBB4A5F0086E4AA /* AccountsViewModel.swift */; };
 		26D8F54A2A6C08D20044398A /* TopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D8F5492A6C08D20044398A /* TopView.swift */; };
 		26DA813224B641A5006C6E23 /* ProfilesAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 26DA813124B641A5006C6E23 /* ProfilesAdapter.mm */; };
 		26DD506D2A86A58E0074E55F /* ActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DD506C2A86A58E0074E55F /* ActionsView.swift */; };
 		26DD506F2A86CFF20074E55F /* ActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DD506E2A86CFF20074E55F /* ActionsViewModel.swift */; };
 		26E0731A2A915A15006B6F03 /* DraggableCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E073192A915A15006B6F03 /* DraggableCaptureView.swift */; };
 		26E0731C2A918F7A006B6F03 /* SwiftUIViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0731B2A918F7A006B6F03 /* SwiftUIViews.swift */; };
+		26E081222BC03AB3001B4C18 /* AccountLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E081212BC03AB3001B4C18 /* AccountLists.swift */; };
+		26E081242BC07824001B4C18 /* RequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E081232BC07824001B4C18 /* RequestsViewModel.swift */; };
+		26E081262BC09534001B4C18 /* RequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E081252BC09534001B4C18 /* RequestsView.swift */; };
+		26EEB19C2BD98CCA00B8B04B /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEB19B2BD98CCA00B8B04B /* SearchBar.swift */; };
+		26EEB19E2BDEDC0C00B8B04B /* UIControllersWrappers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEB19D2BDEDC0C00B8B04B /* UIControllersWrappers.swift */; };
 		26EF35E728E3401800D97E14 /* ReplyHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EF35E628E3401800D97E14 /* ReplyHistory.swift */; };
 		26EF35E928E3847100D97E14 /* MessageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EF35E828E3847100D97E14 /* MessageContentView.swift */; };
 		26EF35EB28E38DA200D97E14 /* MessageContentVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EF35EA28E38DA200D97E14 /* MessageContentVM.swift */; };
@@ -775,7 +784,6 @@
 		1A2D18BF1F29180700B2C785 /* DeviceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceModel.swift; sourceTree = "<group>"; };
 		1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; };
 		1A2D18DC1F29192D00B2C785 /* MessableBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessableBubble.swift; sourceTree = "<group>"; };
-		1A2D18E41F29197100B2C785 /* MessageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; };
 		1A2D18EC1F2919D800B2C785 /* MeViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MeViewController.storyboard; sourceTree = "<group>"; };
 		1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; };
 		1A2D18FB1F292DAD00B2C785 /* SmartListCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SmartListCell.xib; sourceTree = "<group>"; };
@@ -876,6 +884,10 @@
 		265DFB0A292FD41100834B97 /* MessageSwiftUIViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSwiftUIViews.swift; sourceTree = "<group>"; };
 		265DFB1029302A8700834B97 /* ContactMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactMessageView.swift; sourceTree = "<group>"; };
 		265E976226E8F610008801C0 /* NotificationService-debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NotificationService-debug.entitlements"; sourceTree = "<group>"; };
+		26625BB42BA9DF67009D2DDB /* ConversationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ConversationsView.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/ConversationsView.swift; sourceTree = SOURCE_ROOT; };
+		26625BB62BA9DF81009D2DDB /* ConversationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ConversationsViewModel.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Models/ConversationsViewModel.swift; sourceTree = SOURCE_ROOT; };
+		26625BB82BA9FEFC009D2DDB /* SmartListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SmartListContentView.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContentView.swift; sourceTree = SOURCE_ROOT; };
+		26625BBA2BAA0749009D2DDB /* SmartListContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SmartListContainer.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContainer.swift; sourceTree = SOURCE_ROOT; };
 		2662FC78246B1E1700FA7782 /* JamiSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamiSearchView.swift; sourceTree = "<group>"; };
 		2662FC7A246B216B00FA7782 /* JamiSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamiSearchViewModel.swift; sourceTree = "<group>"; };
 		2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IncognitoSmartListViewController.swift; path = Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift; sourceTree = SOURCE_ROOT; };
@@ -987,6 +999,7 @@
 		26D08ABA2696293100E37574 /* RequestsAdapterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestsAdapterDelegate.swift; sourceTree = "<group>"; };
 		26D08ABC2696296300E37574 /* RequestsAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RequestsAdapter.h; sourceTree = "<group>"; };
 		26D08ABD2696296300E37574 /* RequestsAdapter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RequestsAdapter.mm; sourceTree = "<group>"; };
+		26D87CEA2BBB4A5F0086E4AA /* AccountsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountsViewModel.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Models/AccountsViewModel.swift; sourceTree = SOURCE_ROOT; };
 		26D8F5492A6C08D20044398A /* TopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopView.swift; sourceTree = "<group>"; };
 		26DA813024B641A5006C6E23 /* ProfilesAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ProfilesAdapter.h; sourceTree = "<group>"; };
 		26DA813124B641A5006C6E23 /* ProfilesAdapter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ProfilesAdapter.mm; sourceTree = "<group>"; };
@@ -994,6 +1007,11 @@
 		26DD506E2A86CFF20074E55F /* ActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionsViewModel.swift; sourceTree = "<group>"; };
 		26E073192A915A15006B6F03 /* DraggableCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableCaptureView.swift; sourceTree = "<group>"; };
 		26E0731B2A918F7A006B6F03 /* SwiftUIViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViews.swift; sourceTree = "<group>"; };
+		26E081212BC03AB3001B4C18 /* AccountLists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountLists.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/AccountLists.swift; sourceTree = SOURCE_ROOT; };
+		26E081232BC07824001B4C18 /* RequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RequestsViewModel.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Models/RequestsViewModel.swift; sourceTree = SOURCE_ROOT; };
+		26E081252BC09534001B4C18 /* RequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RequestsView.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/RequestsView.swift; sourceTree = SOURCE_ROOT; };
+		26EEB19B2BD98CCA00B8B04B /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SearchBar.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/SearchBar.swift; sourceTree = SOURCE_ROOT; };
+		26EEB19D2BDEDC0C00B8B04B /* UIControllersWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIControllersWrappers.swift; path = Ring/Features/Conversations/SmartList/SwiftUI/Views/UIControllersWrappers.swift; sourceTree = SOURCE_ROOT; };
 		26EF35E628E3401800D97E14 /* ReplyHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyHistory.swift; sourceTree = "<group>"; };
 		26EF35E828E3847100D97E14 /* MessageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentView.swift; sourceTree = "<group>"; };
 		26EF35EA28E38DA200D97E14 /* MessageContentVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentVM.swift; sourceTree = "<group>"; };
@@ -2043,6 +2061,7 @@
 		1A2D18AD1F29151E00B2C785 /* Smartlist */ = {
 			isa = PBXGroup;
 			children = (
+				26625BB12BA9DF0C009D2DDB /* SwiftUI */,
 				1A2D18F91F292DA000B2C785 /* Cells */,
 				1A2D18B01F2915B600B2C785 /* SmartlistViewController.storyboard */,
 				1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */,
@@ -2064,7 +2083,6 @@
 				1A2D18B21F2915C500B2C785 /* ConversationViewController.storyboard */,
 				1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */,
 				1A5DC02D1F3565640075E8EF /* ConversationViewModel.swift */,
-				1A2D18E41F29197100B2C785 /* MessageViewModel.swift */,
 				26D08AB02693474300E37574 /* InvitationViewController.storyboard */,
 				26D08AB42693480200E37574 /* InvitationViewController.swift */,
 				26D08AB62693481C00E37574 /* InvitationViewModel.swift */,
@@ -2339,6 +2357,39 @@
 			path = ViewModels;
 			sourceTree = "<group>";
 		};
+		26625BB12BA9DF0C009D2DDB /* SwiftUI */ = {
+			isa = PBXGroup;
+			children = (
+				26625BB32BA9DF22009D2DDB /* Views */,
+				26625BB22BA9DF17009D2DDB /* Models */,
+			);
+			path = SwiftUI;
+			sourceTree = "<group>";
+		};
+		26625BB22BA9DF17009D2DDB /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				26625BB62BA9DF81009D2DDB /* ConversationsViewModel.swift */,
+				26D87CEA2BBB4A5F0086E4AA /* AccountsViewModel.swift */,
+				26E081232BC07824001B4C18 /* RequestsViewModel.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
+		26625BB32BA9DF22009D2DDB /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				26625BB42BA9DF67009D2DDB /* ConversationsView.swift */,
+				26625BB82BA9FEFC009D2DDB /* SmartListContentView.swift */,
+				26625BBA2BAA0749009D2DDB /* SmartListContainer.swift */,
+				26E081212BC03AB3001B4C18 /* AccountLists.swift */,
+				26E081252BC09534001B4C18 /* RequestsView.swift */,
+				26EEB19B2BD98CCA00B8B04B /* SearchBar.swift */,
+				26EEB19D2BDEDC0C00B8B04B /* UIControllersWrappers.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
 		2662FC77246B1DD600FA7782 /* JamiSearchView */ = {
 			isa = PBXGroup;
 			children = (
@@ -2849,6 +2900,7 @@
 				1A2D18AC1F29149D00B2C785 /* MeCoordinator.swift in Sources */,
 				260AE62529B65A4B00D66D5E /* ContactsUtils.swift in Sources */,
 				1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */,
+				26625BB52BA9DF67009D2DDB /* ConversationsView.swift in Sources */,
 				BB4C6E2629229131001C901A /* ColorExtension.swift in Sources */,
 				269DA09928E23D37007D51D6 /* MessageRowView.swift in Sources */,
 				648AF76D24ED7CA90004D727 /* UITextView+Helpers.swift in Sources */,
@@ -2913,9 +2965,11 @@
 				26EF35E728E3401800D97E14 /* ReplyHistory.swift in Sources */,
 				1A2D18DD1F29192D00B2C785 /* MessableBubble.swift in Sources */,
 				260C73F329196C6C005C513F /* MessageStackVM.swift in Sources */,
+				26625BBB2BAA0749009D2DDB /* SmartListContainer.swift in Sources */,
 				2640BA9A2B974E4B006D189A /* MessageProtocols.swift in Sources */,
 				0E96ED77225D06380016C07D /* GeneralSettingsViewController.swift in Sources */,
 				263F61782B45E8FD00240AEE /* MessageBubbleView.swift in Sources */,
+				26E081242BC07824001B4C18 /* RequestsViewModel.swift in Sources */,
 				264FB66229775B1C00BEFBBF /* SystemService.swift in Sources */,
 				0E99F1A022417A0400CF8BD6 /* JamiURI.swift in Sources */,
 				1A5DC02E1F3565640075E8EF /* ConversationViewModel.swift in Sources */,
@@ -2923,6 +2977,7 @@
 				0EBB72A92034F44200D88F46 /* ProfilesService.swift in Sources */,
 				0E13A91C22B844B100A12A54 /* NSUserActivity+Call.swift in Sources */,
 				1A2D189C1F264AD900B2C785 /* UIViewController+Ring.swift in Sources */,
+				26E081262BC09534001B4C18 /* RequestsView.swift in Sources */,
 				0ECA56852433949C0055D31E /* MigrateAccountViewModel.swift in Sources */,
 				02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */,
 				62B60AF420489E7C001BEACF /* DataTransferService.swift in Sources */,
@@ -2969,6 +3024,7 @@
 				1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */,
 				0E48F9D31FDF150700D6CC08 /* GeneratedInteractionsManager.swift in Sources */,
 				265C436A286254C900B4BE73 /* Constants.swift in Sources */,
+				26E081222BC03AB3001B4C18 /* AccountLists.swift in Sources */,
 				264EA87E2977080300B6FB6F /* LogViewModel.swift in Sources */,
 				62B60AF92048A40D001BEACF /* DataTransferAdapter.mm in Sources */,
 				1A5DC0371F35675E0075E8EF /* ContactRequestCell.swift in Sources */,
@@ -3000,6 +3056,7 @@
 				1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */,
 				BBB76E412966062C00A42DF5 /* MapView.swift in Sources */,
 				62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */,
+				26625BB72BA9DF81009D2DDB /* ConversationsViewModel.swift in Sources */,
 				1DF75AC6296E0C2A0055EA87 /* AddMoreParticipantsInSwarm.swift in Sources */,
 				BB6690072A99069900875848 /* WelcomeViewController.swift in Sources */,
 				1A2D18B71F29164700B2C785 /* SmartlistViewModel.swift in Sources */,
@@ -3024,6 +3081,7 @@
 				265DFB0B292FD41100834B97 /* MessageSwiftUIViews.swift in Sources */,
 				564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */,
 				26D08AB72693481C00E37574 /* InvitationViewModel.swift in Sources */,
+				26625BB92BA9FEFC009D2DDB /* SmartListContentView.swift in Sources */,
 				26074FD924F7FF9500374570 /* PreviewViewController.swift in Sources */,
 				0E320D54224ADFD00070B515 /* DialpadViewModel.swift in Sources */,
 				0EB1A5D11F8EBE23009923E2 /* DeviceCell.swift in Sources */,
@@ -3093,9 +3151,9 @@
 				0E49096E1FEAC0DE005CAA50 /* CallsService.swift in Sources */,
 				0273C2FF1E0C438F00CF00BA /* AccountAdapterDelegate.swift in Sources */,
 				1A2D18A41F27EF5200B2C785 /* AppCoordinator.swift in Sources */,
+				26D87CEB2BBB4A5F0086E4AA /* AccountsViewModel.swift in Sources */,
 				1A2D18C31F29180700B2C785 /* AccountModel.swift in Sources */,
 				62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */,
-				1A2D18EB1F29197100B2C785 /* MessageViewModel.swift in Sources */,
 				02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */,
 				62E55B6F1F793ADE00D3FEF4 /* AvatarsColors.swift in Sources */,
 				BB06BD8129D491F80064F0FC /* CustomAnnotationModel.swift in Sources */,
@@ -3108,9 +3166,11 @@
 				564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */,
 				26BCBBD32A964ABC0001EE38 /* ConferenceActionsModel.swift in Sources */,
 				0E6F5453223C3C7500ECC3CE /* AccountItemView.swift in Sources */,
+				26EEB19C2BD98CCA00B8B04B /* SearchBar.swift in Sources */,
 				0EF49AA123828CBC0064CD98 /* ParticipantProfileInfo.swift in Sources */,
 				BB68E6602AB1F03000F02AB7 /* ScreenHelper.swift in Sources */,
 				0EF49AA123828CBC0064CD98 /* ParticipantProfileInfo.swift in Sources */,
+				26EEB19E2BDEDC0C00B8B04B /* UIControllersWrappers.swift in Sources */,
 				26DD506F2A86CFF20074E55F /* ActionsViewModel.swift in Sources */,
 				0E8E9A0520483E1200DA8E8B /* TitleView.swift in Sources */,
 				264FB66629775B9B00BEFBBF /* SystemAdapterDelegate.swift in Sources */,
diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift
index 65cc2d4..52b8a2c 100644
--- a/Ring/Ring/Constants/Generated/Strings.swift
+++ b/Ring/Ring/Constants/Generated/Strings.swift
@@ -331,6 +331,10 @@
     internal static let startVideoCall = L10n.tr("Localizable", "contactPage.startVideoCall", fallback: "Start Video Call")
   }
   internal enum Conversation {
+    /// Add to Contacts
+    internal static let addToContactsButton = L10n.tr("Localizable", "conversation.addToContactsButton", fallback: "Add to Contacts")
+    /// Add to contacts?
+    internal static let addToContactsLabel = L10n.tr("Localizable", "conversation.addToContactsLabel", fallback: "Add to contacts?")
     /// deleted a message
     internal static let deletedMessage = L10n.tr("Localizable", "conversation.deletedMessage", fallback: "deleted a message")
     /// edited
@@ -351,6 +355,8 @@
     internal static func notContact(_ p1: Any) -> String {
       return L10n.tr("Localizable", "conversation.notContact", String(describing: p1), fallback: "%@ is not in your contact list.")
     }
+    /// is not in your contact list
+    internal static let notContactLabel = L10n.tr("Localizable", "conversation.notContactLabel", fallback: "is not in your contact list")
     /// %@ sent you a request for a conversation.
     internal static func receivedRequest(_ p1: Any) -> String {
       return L10n.tr("Localizable", "conversation.receivedRequest", String(describing: p1), fallback: "%@ sent you a request for a conversation.")
@@ -586,8 +592,18 @@
     internal static let username = L10n.tr("Localizable", "global.username", fallback: "Username")
   }
   internal enum Invitations {
+    /// accepted
+    internal static let accepted = L10n.tr("Localizable", "invitations.accepted", fallback: "accepted")
+    /// banned
+    internal static let banned = L10n.tr("Localizable", "invitations.banned", fallback: "banned")
+    /// Invitations received
+    internal static let list = L10n.tr("Localizable", "invitations.list", fallback: "Invitations received")
     /// No invitations
     internal static let noInvitations = L10n.tr("Localizable", "invitations.noInvitations", fallback: "No invitations")
+    /// pending
+    internal static let pending = L10n.tr("Localizable", "invitations.pending", fallback: "pending")
+    /// refused
+    internal static let refused = L10n.tr("Localizable", "invitations.refused", fallback: "refused")
   }
   internal enum LinkDevice {
     /// An error occurred during the export
@@ -688,12 +704,26 @@
     internal static let disableDonation = L10n.tr("Localizable", "smartlist.disableDonation", fallback: "Not now")
     /// If you enjoy using Jami and believe in our mission, would you make a donation?
     internal static let donationExplanation = L10n.tr("Localizable", "smartlist.donationExplanation", fallback: "If you enjoy using Jami and believe in our mission, would you make a donation?")
+    /// conversation in synchronization
+    internal static let inSynchronization = L10n.tr("Localizable", "smartlist.inSynchronization", fallback: "conversation in synchronization")
+    /// Invitations received
+    internal static let invitationReceived = L10n.tr("Localizable", "smartlist.invitationReceived", fallback: "Invitations received")
     /// Invitations
     internal static let invitations = L10n.tr("Localizable", "smartlist.invitations", fallback: "Invitations")
     /// Invite friends
     internal static let inviteFriends = L10n.tr("Localizable", "smartlist.inviteFriends", fallback: "Invite friends")
+    /// Search Result
+    internal static let jamsResults = L10n.tr("Localizable", "smartlist.jamsResults", fallback: "Search Result")
+    /// New Contact
+    internal static let newContact = L10n.tr("Localizable", "smartlist.newContact", fallback: "New Contact")
+    /// New Message
+    internal static let newMessage = L10n.tr("Localizable", "smartlist.newMessage", fallback: "New Message")
+    /// New Swarm
+    internal static let newSwarm = L10n.tr("Localizable", "smartlist.newSwarm", fallback: "New Swarm")
     /// No conversations
     internal static let noConversation = L10n.tr("Localizable", "smartlist.noConversation", fallback: "No conversations")
+    /// No conversations match your search
+    internal static let noConversationsFound = L10n.tr("Localizable", "smartlist.noConversationsFound", fallback: "No conversations match your search")
     /// No network connectivity
     internal static let noNetworkConnectivity = L10n.tr("Localizable", "smartlist.noNetworkConnectivity", fallback: "No network connectivity")
     /// Selected contact does not have any number
diff --git a/Ring/Ring/Extensions/ColorExtension.swift b/Ring/Ring/Extensions/ColorExtension.swift
index 91d86e4..d277ea4 100644
--- a/Ring/Ring/Extensions/ColorExtension.swift
+++ b/Ring/Ring/Extensions/ColorExtension.swift
@@ -86,3 +86,18 @@
         }
     }
 }
+
+extension Color {
+    static let jamiPrimaryControl = Color("jamiPrimaryControl")
+    static let jamiSecondaryControl = Color("jamiSecondaryControl")
+    static let jamiTertiaryControl = Color("jamiTertiaryControl")
+    static let jamiRequestsColor = Color("jamiRequestsColor")
+    static let jamiColor = Color("jami")
+    static let availablePresenceColor = Color(UIColor.availablePresenceColor)
+    static let onlinePresenceColor = Color(UIColor.onlinePresenceColor)
+    static let unreadMessageColorText = Color(UIColor(hexString: "CC0022")!)
+    static let unreadMessageBackground = Color(UIColor(hexString: "EED4D8")!)
+    static let networkAlertBackground = Color(UIColor(red: 245, green: 110, blue: 88, alpha: 1))
+    static let requestBadgeForeground = Color("requestBadgeForeground")
+    static let requestsBadgeBackground = Color("requestsBadgeBackground")
+}
diff --git a/Ring/Ring/Extensions/Date+Helpers.swift b/Ring/Ring/Extensions/Date+Helpers.swift
index 5b12054..08a7425 100644
--- a/Ring/Ring/Extensions/Date+Helpers.swift
+++ b/Ring/Ring/Extensions/Date+Helpers.swift
@@ -43,4 +43,38 @@
         string += String(format: "%02d:%02d", min, Int(sec))
         return string
     }
+
+    func conversationTimestamp() -> String {
+        var dateFormatter: DateFormatter = {
+            let formatter = DateFormatter()
+            formatter.dateStyle = .medium
+            return formatter
+        }()
+        var hourFormatter: DateFormatter = {
+            let formatter = DateFormatter()
+            formatter.dateFormat = "HH:mm"
+            return formatter
+        }()
+        let dateToday = Date()
+        var dateString = ""
+        let todayWeekOfYear = Calendar.current.component(.weekOfYear, from: dateToday)
+        let todayDay = Calendar.current.component(.day, from: dateToday)
+        let todayMonth = Calendar.current.component(.month, from: dateToday)
+        let todayYear = Calendar.current.component(.year, from: dateToday)
+        let weekOfYear = Calendar.current.component(.weekOfYear, from: self)
+        let day = Calendar.current.component(.day, from: self)
+        let month = Calendar.current.component(.month, from: self)
+        let year = Calendar.current.component(.year, from: self)
+        if todayDay == day && todayMonth == month && todayYear == year {
+            dateString = hourFormatter.string(from: self)
+        } else if day == todayDay - 1 {
+            dateString = L10n.Smartlist.yesterday
+        } else if todayYear == year && todayWeekOfYear == weekOfYear {
+            dateString = self.dayOfWeek()
+        } else {
+            dateString = dateFormatter.string(from: self)
+        }
+
+        return dateString
+    }
 }
diff --git a/Ring/Ring/Extensions/UIImage+Helpers.swift b/Ring/Ring/Extensions/UIImage+Helpers.swift
index a53793c..3a98f61 100644
--- a/Ring/Ring/Extensions/UIImage+Helpers.swift
+++ b/Ring/Ring/Extensions/UIImage+Helpers.swift
@@ -265,6 +265,10 @@
 
     func fillJamiBackgroundColor(inset: CGFloat) -> UIImage {
         let color = UIColor.jamiMain
+        return self.fillBackgroundColor(color: color, inset: inset)
+    }
+
+    func fillBackgroundColor(color: UIColor, inset: CGFloat) -> UIImage {
         let newSize = CGSize(width: self.size.width + 2 * inset, height: self.size.height + 2 * inset)
         let drawingRect = CGRect(x: inset, y: inset, width: self.size.width, height: self.size.height)
 
@@ -380,6 +384,17 @@
         return image
     }
 
+    class func createSwarmAvatar(convId: String, size: CGSize) -> UIImage {
+        let image = UIImage(systemName: "person.2")!
+        let scanner = Scanner(string: convId.toMD5HexString().prefixString())
+        var index: UInt64 = 0
+        if scanner.scanHexInt64(&index) {
+            let fbaBGColor = avatarColors[Int(index)]
+            return image.fillBackgroundColor(color: fbaBGColor, inset: 10)
+        }
+        return image
+    }
+
     class func createGroupAvatar(username: String, size: CGSize) -> UIImage {
         let scanner = Scanner(string: username.toMD5HexString().prefixString())
         var index: UInt64 = 0
diff --git a/Ring/Ring/Extensions/View+Helpers.swift b/Ring/Ring/Extensions/View+Helpers.swift
index 35d5045..c208d6e 100644
--- a/Ring/Ring/Extensions/View+Helpers.swift
+++ b/Ring/Ring/Extensions/View+Helpers.swift
@@ -99,6 +99,58 @@
     }
 }
 
+struct PlatformAdaptiveNavView<Content: View>: View {
+    let content: () -> Content
+
+    var body: some View {
+        if #available(iOS 16.0, *) {
+            NavigationStack {
+                content()
+            }
+        } else {
+            NavigationView {
+                content()
+            }
+        }
+    }
+}
+
+struct SlideTransition: ViewModifier {
+    let directionUp: Bool
+
+    func body(content: Content) -> some View {
+        content
+            .transition(.asymmetric(
+                insertion: .move(edge: directionUp ? .bottom : .top),
+                removal: .move(edge: directionUp ? .top : .bottom)
+            ))
+    }
+}
+
+extension View {
+    func applySlideTransition(directionUp: Bool) -> some View {
+        self.modifier(SlideTransition(directionUp: directionUp))
+    }
+}
+
+struct RowSeparatorHiddenModifier: ViewModifier {
+    func body(content: Content) -> some View {
+        if #available(iOS 15.0, *) {
+            content
+                .listRowSeparator(.hidden)
+                .listRowSeparatorTint(.clear)
+        } else {
+            content
+        }
+    }
+}
+
+extension View {
+    func hideRowSeparator() -> some View {
+        self.modifier(RowSeparatorHiddenModifier())
+    }
+}
+
 extension Publishers {
     static var keyboardHeight: AnyPublisher<CGFloat, Never> {
         let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift
index a177947..b55e5d7 100644
--- a/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift
+++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift
@@ -191,7 +191,6 @@
         conversationViewModel.displayName.accept(name)
         conversationViewModel.profileImageData.accept(item.profileImageData.value)
         conversationViewModel.conversation = conversation
-        conversationViewModel.request = item.request
         self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: conversationViewModel))
     }
 }
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index a71591f..815a022 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -87,22 +87,35 @@
 
     private lazy var locationManager: CLLocationManager = { return CLLocationManager() }()
 
-    func setIsComposing(isComposing: Bool) {
-        self.viewModel.setIsComposingMsg(isComposing: isComposing)
-    }
-
     override func viewDidLoad() {
         super.viewDidLoad()
         self.configureNavigationBar()
         self.setupUI()
         self.setupBindings()
-        NotificationCenter.default.addObserver(self,
-                                               selector: #selector(applicationWillResignActive),
-                                               name: UIApplication.willResignActiveNotification,
-                                               object: nil)
         screenTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenTapped))
         self.view.addGestureRecognizer(screenTapRecognizer)
+    }
 
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        if !self.swiftUIViewAdded {
+            self.addSwiftUIView()
+        }
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        self.navigationController?.navigationBar.shadowImage = UIImage()
+        self.navigationController?.navigationBar.layer.shadowOpacity = 0
+        self.viewModel.setMessagesAsRead()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value,
+                           displayName: self.viewModel.displayName.value,
+                           username: self.viewModel.userName.value)
+        self.updateNavigationBarShadow()
     }
 
     @objc
@@ -112,14 +125,7 @@
 
     private func addSwiftUIView() {
         swiftUIViewAdded = true
-        let transferHelper = TransferHelper(dataTransferService: self.viewModel.dataTransferService,
-                                            conversationViewModel: self.viewModel)
-        let swiftUIModel = MessagesListVM(injectionBag: self.viewModel.injectionBag,
-                                          conversation: self.viewModel.conversation,
-                                          transferHelper: transferHelper,
-                                          bestName: self.viewModel.bestName,
-                                          screenTapped: tapAction.asObservable())
-        swiftUIModel.hideNavigationBar
+        self.viewModel.swiftUIModel.hideNavigationBar
             .subscribe(onNext: { [weak self] (hide) in
                 guard let self = self else { return }
                 if self.navigationItem.rightBarButtonItems?.isEmpty == hide { return }
@@ -139,7 +145,9 @@
             })
             .disposed(by: self.disposeBag)
 
-        swiftUIModel.messagePanelState
+        self.viewModel.swiftUIModel.subscribeScreenTapped(screenTapped: tapAction.asObservable())
+
+        self.viewModel.swiftUIModel.messagePanelState
             .subscribe(onNext: { [weak self] (state) in
                 guard let self = self, let state = state as? MessagePanelState else { return }
                 switch state {
@@ -162,7 +170,7 @@
                 }
             })
             .disposed(by: self.disposeBag)
-        swiftUIModel.contextMenuState
+        self.viewModel.swiftUIModel.contextMenuState
             .subscribe(onNext: { [weak self] (state) in
                 guard let self = self, let state = state as? ContextMenu else { return }
                 switch state {
@@ -187,13 +195,13 @@
             .disposed(by: self.disposeBag)
         self.viewModel.conversationCreated
             .observe(on: MainScheduler.instance)
-            .subscribe { [weak self, weak swiftUIModel] update in
-                guard let self = self, let swiftUIModel = swiftUIModel, update else { return }
-                swiftUIModel.conversation = self.viewModel.conversation
+            .subscribe { [weak self] update in
+                guard let self = self, update else { return }
+                self.viewModel.swiftUIModel.conversation = self.viewModel.conversation
             } onError: { _ in
             }
             .disposed(by: self.disposeBag)
-        let messageListView = MessagesListView(model: swiftUIModel)
+        let messageListView = MessagesListView(model: self.viewModel.swiftUIModel)
         let swiftUIView = UIHostingController(rootView: messageListView)
         addChild(swiftUIView)
         swiftUIView.view.frame = self.view.frame
@@ -211,19 +219,6 @@
         }
     }
 
-    @objc
-    private func applicationWillResignActive() {
-        self.viewModel.setIsComposingMsg(isComposing: false)
-    }
-
-    override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-        self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value,
-                           displayName: self.viewModel.displayName.value,
-                           username: self.viewModel.userName.value)
-        self.updateNavigationBarShadow()
-    }
-
     private func importDocument() {
         currentDocumentPickerMode = .picking
         let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.item])
@@ -644,21 +639,6 @@
         self.viewModel.startAudioCall()
     }
 
-    override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
-        if !self.swiftUIViewAdded {
-            self.addSwiftUIView()
-        }
-    }
-
-    override func viewWillDisappear(_ animated: Bool) {
-        super.viewWillDisappear(animated)
-        self.navigationController?.navigationBar.shadowImage = UIImage()
-        self.navigationController?.navigationBar.layer.shadowOpacity = 0
-        self.viewModel.setIsComposingMsg(isComposing: false)
-        self.viewModel.setMessagesAsRead()
-    }
-
     private func messagesLoadingFinished() {
         self.spinnerView.isHidden = true
     }
@@ -672,22 +652,6 @@
             } onError: { _ in
             }
             .disposed(by: self.disposeBag)
-        self.viewModel.showInvitation
-            .observe(on: MainScheduler.instance)
-            .subscribe { [weak self] show in
-                guard let self = self else { return }
-                if show {
-                    if self.view.window?.rootViewController is InvitationViewController {
-                        return
-                    }
-                    self.navigationItem.rightBarButtonItems = []
-                    self.viewModel.openInvitationView(parentView: self)
-                } else {
-                    self.setRightNavigationButtons()
-                }
-            } onError: { _ in
-            }
-            .disposed(by: self.disposeBag)
         self.viewModel.synchronizing
             .startWith(self.viewModel.synchronizing.value)
             .observe(on: MainScheduler.instance)
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
index f247b39..1e16bad 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2021 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2024 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
@@ -26,10 +26,40 @@
 import RxSwift
 import RxCocoa
 import SwiftyBeaver
+import SwiftUI
+
+enum MessageSequencing {
+    case singleMessage
+    case firstOfSequence
+    case lastOfSequence
+    case middleOfSequence
+    case unknown
+}
+
+enum GeneratedMessageType: String {
+    case receivedContactRequest = "Contact request received"
+    case contactAdded = "Contact added"
+    case missedIncomingCall = "Missed incoming call"
+    case missedOutgoingCall = "Missed outgoing call"
+    case incomingCall = "Incoming call"
+    case outgoingCall = "Outgoing call"
+}
 
 // swiftlint:disable type_body_length
 // swiftlint:disable file_length
-class ConversationViewModel: Stateable, ViewModel {
+class ConversationViewModel: Stateable, ViewModel, ObservableObject, Identifiable {
+
+    @Published var avatar: UIImage?
+    @Published var name: String = ""
+    @Published var lastMessage: String = ""
+    @Published var lastMessageDate: String = ""
+    @Published var unreadMessages: Int = 0
+    @Published var presence: PresenceStatus = .offline
+    @Published var isSynchronizing: Bool = false
+
+    func getDefaultAvatar() -> UIImage {
+        return UIImage.createContactAvatar(username: (self.displayName.value?.isEmpty ?? true) ? self.userName.value : self.displayName.value!)
+    }
 
     /// Logger
     private let log = SwiftyBeaver.self
@@ -49,23 +79,11 @@
 
     internal let disposeBag = DisposeBag()
 
-    private var players = [String: PlayerViewModel]()
-
-    func getPlayer(messageID: String) -> PlayerViewModel? {
-        return players[messageID]
-    }
-    func setPlayer(messageID: String, player: PlayerViewModel) { players[messageID] = player }
     func closeAllPlayers() {
-        let queue = DispatchQueue.global(qos: .default)
-        queue.sync {
-            self.players.values.forEach { (player) in
-                player.closePlayer()
-            }
-            self.players.removeAll()
-        }
+        self.swiftUIModel.transferHelper.closeAllPlayers()
     }
 
-    let showInvitation = BehaviorRelay<Bool>(value: false)
+    let isTemporary = BehaviorRelay<Bool>(value: false)
 
     let showIncomingLocationSharing = BehaviorRelay<Bool>(value: false)
     let showOutgoingLocationSharing = BehaviorRelay<Bool>(value: false)
@@ -77,24 +95,6 @@
 
     var synchronizing = BehaviorRelay<Bool>(value: false)
 
-    lazy var typingIndicator: Observable<Bool> = {
-        return self.conversationsService
-            .sharedResponseStream
-            .filter({ [weak self] (event) -> Bool in
-                return event.eventType == ServiceEventType.messageTypingIndicator &&
-                event.getEventInput(ServiceEventInput.accountId) == self?.conversation.accountId &&
-                event.getEventInput(ServiceEventInput.peerUri) == self?.conversation.hash
-            })
-            .map({ (event) -> Bool in
-                if let status: Int = event.getEventInput(ServiceEventInput.state), status == 1 {
-                    return true
-                }
-                return false
-            })
-    }()
-
-    private var isJamsAccount: Bool { self.accountService.isJams(for: self.conversation.accountId) }
-
     var isAccountSip: Bool = false
 
     var displayName = BehaviorRelay<String?>(value: nil)
@@ -104,15 +104,14 @@
             .combineLatest(userName.asObservable(),
                            displayName.asObservable(),
                            resultSelector: {(userName, displayname) in
-                            guard let displayname = displayname, !displayname.isEmpty else { return userName }
+                            guard let displayname = displayname, !displayname.isEmpty else {
+                                return userName }
                             return displayname
                            })
     }()
 
     /// Group's image data
     var profileImageData = BehaviorRelay<Data?>(value: nil)
-    /// My profile's image data
-    var myOwnProfileImageData: Data?
 
     var contactPresence = BehaviorRelay<PresenceStatus>(value: .offline)
     var swarmInfo: SwarmInfoProtocol?
@@ -128,14 +127,69 @@
         self.dataTransferService = injectionBag.dataTransferService
         self.callService = injectionBag.callService
         self.locationSharingService = injectionBag.locationSharingService
+        let transferHelper = TransferHelper(injectionBag: injectionBag)
+
+        swiftUIModel = MessagesListVM(injectionBag: self.injectionBag,
+                                      transferHelper: transferHelper)
+        swiftUIModel.subscribeBestName(bestName: self.bestName)
+        self.bestName
+            .share()
+            .asObservable()
+            .observe(on: MainScheduler.instance)
+            .startWith((self.displayName.value?.isEmpty ?? true) ? self.userName.value : self.displayName.value!)
+            .subscribe(onNext: { [weak self] bestName in
+                let name = bestName.replacingOccurrences(of: "\0", with: "")
+                guard !name.isEmpty else { return }
+                self?.name = name
+                self?.swiftUIModel.name = name
+            })
+            .disposed(by: self.disposeBag)
+
+        self.profileImageData
+            .share()
+            .asObservable()
+            .observe(on: MainScheduler.instance)
+            .startWith(self.profileImageData.value)
+            .subscribe(onNext: { [weak self] imageData in
+                if let imageData = imageData, !imageData.isEmpty {
+                    if let image = UIImage(data: imageData) {
+                        self?.avatar = image
+                    }
+                }
+            })
+            .disposed(by: self.disposeBag)
+
+        self.lastMessageObservable
+            .share()
+            .asObservable()
+            .observe(on: MainScheduler.instance)
+            .startWith(swiftUIModel.lastMessage.value)
+            .subscribe(onNext: { [weak self] mesage in
+                self?.lastMessage = mesage
+            })
+            .disposed(by: self.disposeBag)
+
+        self.lastMessageDateObservable
+            .share()
+            .asObservable()
+            .observe(on: MainScheduler.instance)
+            .startWith(swiftUIModel.lastMessageDate.value)
+            .subscribe(onNext: { [weak self] mesageDate in
+                self?.lastMessageDate = mesageDate
+            })
+            .disposed(by: self.disposeBag)
+
+        self.isTemporary
+            .observe(on: MainScheduler.instance)
+            .subscribe { [weak self] show in
+                self?.swiftUIModel.isTemporary = show
+            } onError: { _ in
+            }
+            .disposed(by: self.disposeBag)
     }
 
     private func setConversation(_ conversation: ConversationModel) {
-       // if self.conversation != nil {
-            self.conversation = conversation
-//        } else {
-//            self.conversation = BehaviorRelay(value: conversation)
-//        }
+        self.conversation = conversation
     }
 
     convenience init(with injectionBag: InjectionBag, conversation: ConversationModel, user: JamiSearchViewModel.JamsUserSearchModel) {
@@ -143,35 +197,32 @@
         self.userName.accept(user.username)
         self.displayName.accept(user.firstName + " " + user.lastName)
         self.profileImageData.accept(user.profilePicture)
+        self.swiftUIModel.jamsAvatarData = user.profilePicture
+        self.swiftUIModel.jamsName = user.firstName + " " + user.lastName
         self.setConversation(conversation) // required to trigger the didSet
     }
 
-    var request: RequestModel? {
-        didSet {
-            if request != nil && !self.showInvitation.value {
-                self.showInvitation.accept(true)
-            }
-        }
+    var swiftUIModel: MessagesListVM
+
+    var lastMessageObservable: Observable <String> {
+        return swiftUIModel.lastMessage.asObservable()
+    }
+
+    var lastMessageDateObservable: Observable <String> {
+        return swiftUIModel.lastMessageDate.asObservable()
     }
 
     var conversation: ConversationModel! {
         didSet {
             self.subscribeUnreadMessages()
-            self.subscribeProfileServiceMyPhoto()
+            self.swiftUIModel.conversation = conversation
 
             guard let account = self.accountService.getAccount(fromAccountId: self.conversation.accountId) else { return }
             if account.type == AccountType.sip {
                 self.userName.accept(self.conversation.hash)
                 self.isAccountSip = true
-                self.subscribeLastMessagesUpdate()
                 return
             }
-            ///
-            let showInv = self.request != nil || self.conversation.id.isEmpty
-            if self.showInvitation.value != showInv {
-                self.showInvitation.accept(showInv)
-            }
-
             self.subscribePresenceServiceContactPresence()
             if self.shouldCreateSwarmInfo() {
                 self.createSwarmInfo()
@@ -199,10 +250,8 @@
                  */
                 subscribeConversationReady()
             }
-            subscribeLastMessagesUpdate()
             subscribeConversationSynchronization()
             subscribeLocationEvents()
-            // self.subscribeConversationServiceTypingIndicator()
         }
     }
 
@@ -216,6 +265,7 @@
             .subscribe { [weak self] synchronizing in
                 guard let self = self else { return }
                 self.synchronizing.accept(synchronizing)
+                self.isSynchronizing = synchronizing
             } onError: { _ in
             }
             .disposed(by: self.disposeBag)
@@ -246,7 +296,6 @@
     func createSwarmInfo() {
         self.swarmInfo = SwarmInfo(injectionBag: self.injectionBag, conversation: self.conversation)
         self.swarmInfo!.finalAvatar.share()
-            .observe(on: MainScheduler.instance)
             .subscribe { [weak self] image in
                 self?.profileImageData.accept(image.pngData())
             } onError: { _ in
@@ -280,59 +329,6 @@
             .disposed(by: self.disposeBag)
     }
 
-    private func subscribeLastMessagesUpdate() {
-        conversation.newMessages
-            .subscribe { [weak self] _ in
-                guard let self = self, let lastMessage = self.conversation.lastMessage else { return }
-                self.lastMessage.accept(lastMessage.content)
-                let lastMessageDate = lastMessage.receivedDate
-                let dateToday = Date()
-                var dateString = ""
-                let todayWeekOfYear = Calendar.current.component(.weekOfYear, from: dateToday)
-                let todayDay = Calendar.current.component(.day, from: dateToday)
-                let todayMonth = Calendar.current.component(.month, from: dateToday)
-                let todayYear = Calendar.current.component(.year, from: dateToday)
-                let weekOfYear = Calendar.current.component(.weekOfYear, from: lastMessageDate)
-                let day = Calendar.current.component(.day, from: lastMessageDate)
-                let month = Calendar.current.component(.month, from: lastMessageDate)
-                let year = Calendar.current.component(.year, from: lastMessageDate)
-                if todayDay == day && todayMonth == month && todayYear == year {
-                    dateString = self.hourFormatter.string(from: lastMessageDate)
-                } else if day == todayDay - 1 {
-                    dateString = L10n.Smartlist.yesterday
-                } else if todayYear == year && todayWeekOfYear == weekOfYear {
-                    dateString = lastMessageDate.dayOfWeek()
-                } else {
-                    dateString = self.dateFormatter.string(from: lastMessageDate)
-                }
-                self.lastMessageReceivedDate.accept(dateString)
-            } onError: { _ in
-            }
-            .disposed(by: self.disposeBag)
-    }
-
-    /// Displays the entire date ( for messages received before the current week )
-    private lazy var dateFormatter: DateFormatter = {
-        let formatter = DateFormatter()
-        formatter.dateStyle = .medium
-        return formatter
-    }()
-    /// Displays the hour of the message reception ( for messages received today )
-    private lazy var hourFormatter: DateFormatter = {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm"
-        return formatter
-    }()
-
-    var unreadMessages = BehaviorRelay<String>(value: "")
-
-    var lastMessage = BehaviorRelay<String>(value: "")
-    var lastMessageReceivedDate = BehaviorRelay<String>(value: "")
-
-    var hideNewMessagesLabel = BehaviorRelay<Bool>(value: true)
-
-    var hideDate: Bool { self.conversation.messages.isEmpty }
-
     func editMessage(content: String, messageId: String) {
         guard let conversation = self.conversation else { return }
         self.conversationsService.editSwarmMessage(conversationId: conversation.id, accountId: conversation.accountId, message: content, parentId: messageId)
@@ -402,7 +398,7 @@
     }
 
     func showContactInfo() {
-        if self.showInvitation.value {
+        if self.isTemporary.value {
             return
         }
         self.closeAllPlayers()
@@ -495,50 +491,6 @@
         self.closeAllPlayers()
     }
 
-    func setIsComposingMsg(isComposing: Bool) {
-        //        if composingMessage == isComposing {
-        //            return
-        //        }
-        //        composingMessage = isComposing
-        //        guard let account = self.accountService.currentAccount else { return }
-        //        conversationsService
-        //            .setIsComposingMsg(to: self.conversation.participantUri,
-        //                               from: account.id,
-        //                               isComposing: isComposing)
-    }
-
-    func addComposingIndicatorMsg() {
-        //        if peerComposingMessage {
-        //            return
-        //        }
-        //        peerComposingMessage = true
-        //        var messagesValue = self.messages.value
-        //        let msgModel = MessageModel(withId: "",
-        //                                    receivedDate: Date(),
-        //                                    content: "       ",
-        //                                    authorURI: self.conversation.participantUri,
-        //                                    incoming: true)
-        //        let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel, isLastDisplayed: false)
-        //        composingIndicator.isComposingIndicator = true
-        //        messagesValue.append(composingIndicator)
-        //        self.messages.accept(messagesValue)
-    }
-
-    var composingMessage: Bool = false
-    // var peerComposingMessage: Bool = false
-
-    func removeComposingIndicatorMsg() {
-        //        if !peerComposingMessage {
-        //            return
-        //        }
-        //        peerComposingMessage = false
-        //        let messagesValue = self.messages.value
-        //        let conversationsMsg = messagesValue.filter { (messageModel) -> Bool in
-        //            !messageModel.isComposingIndicator
-        //        }
-        //        self.messages.accept(conversationsMsg)
-    }
-
     var myContactsLocation = BehaviorSubject<CLLocationCoordinate2D?>(value: nil)
     let shouldDismiss = BehaviorRelay<Bool>(value: false)
 
@@ -547,56 +499,11 @@
     }
 
     var conversationCreated = BehaviorRelay(value: true)
-
-    func openInvitationView(parentView: UIViewController) {
-        let name = self.displayName.value?.isEmpty ?? true ? self.userName.value : self.displayName.value ?? ""
-        let handler: ((String) -> Void) = { [weak self] conversationId in
-            guard let self = self else { return }
-            guard let conversation = self.conversationsService.getConversationForId(conversationId: conversationId, accountId: self.conversation.accountId),
-                  !conversationId.isEmpty else {
-                self.shouldDismiss.accept(true)
-                return
-            }
-            self.request = nil
-            self.conversation = conversation
-            self.conversationCreated.accept(true)
-            if self.showInvitation.value {
-                self.showInvitation.accept(false)
-            }
-        }
-        if let request = self.request {
-            // show incoming request
-            self.stateSubject.onNext(ConversationState.openIncomingInvitationView(displayName: name, request: request, parentView: parentView, invitationHandeledCB: handler))
-        } else if self.conversation.id.isEmpty {
-            // send invitation for search result
-            let alias = (self.conversation.type == .jams ? self.displayName.value : "") ?? ""
-            self.stateSubject.onNext(ConversationState
-                                        .openOutgoingInvitationView(displayName: name, alias: alias, avatar: self.profileImageData.value,
-                                                                    contactJamiId: self.conversation.hash,
-                                                                    accountId: self.conversation.accountId,
-                                                                    parentView: parentView,
-                                                                    invitationHandeledCB: handler))
-        }
-    }
 }
 
 // MARK: Conversation didSet functions
 extension ConversationViewModel {
 
-    private func subscribeProfileServiceMyPhoto() {
-        guard let account = self.accountService.currentAccount else { return }
-        self.profileService
-            .getAccountProfile(accountId: account.id)
-            .subscribe(onNext: { [weak self] profile in
-                guard let self = self else { return }
-                if let photo = profile.photo,
-                   let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
-                    self.myOwnProfileImageData = data
-                }
-            })
-            .disposed(by: self.disposeBag)
-    }
-
     private func subscribePresenceServiceContactPresence() {
         if !self.conversation.isDialog() {
             return
@@ -611,7 +518,7 @@
                 .filter({ [weak self] serviceEvent in
                     guard let uri: String = serviceEvent.getEventInput(ServiceEventInput.uri),
                           let accountID: String = serviceEvent.getEventInput(ServiceEventInput.accountId),
-                    let conversation = self?.conversation else { return false }
+                          let conversation = self?.conversation else { return false }
                     return uri == conversation.getParticipants().first?.jamiId && accountID == conversation.accountId
                 })
                 .subscribe(onNext: { [weak self] _ in
@@ -620,14 +527,21 @@
                 .disposed(by: self.disposeBag)
             self.presenceService.subscribeBuddy(withAccountId: self.conversation.accountId, withUri: self.conversation.getParticipants().first!.jamiId, withFlag: true)
         }
+        self.contactPresence
+            .observe(on: MainScheduler.instance)
+            .subscribe { [weak self] presence in
+                self?.presence = presence
+            } onError: { _ in
+            }
+            .disposed(by: self.disposeBag)
     }
 
     private func subscribeUnreadMessages() {
         self.conversation.numberOfUnreadMessages
+            .observe(on: MainScheduler.instance)
             .subscribe { [weak self] unreadMessages in
                 guard let self = self else { return }
-                self.hideNewMessagesLabel.accept(unreadMessages == 0)
-                self.unreadMessages.accept(String(unreadMessages.description))
+                self.unreadMessages = unreadMessages
             } onError: { _ in
             }
             .disposed(by: self.disposeBag)
@@ -651,8 +565,8 @@
             .usernameLookupStatus
             .filter({ [weak self] lookupNameResponse in
                 return lookupNameResponse.address != nil &&
-                (lookupNameResponse.address == self?.conversation.getParticipants().first?.jamiId ||
-                 lookupNameResponse.address == self?.conversation.getParticipants().first?.jamiId)
+                    (lookupNameResponse.address == self?.conversation.getParticipants().first?.jamiId ||
+                        lookupNameResponse.address == self?.conversation.getParticipants().first?.jamiId)
             })
             .subscribe(onNext: { [weak self] lookupNameResponse in
                 if let name = lookupNameResponse.name, !name.isEmpty {
@@ -664,18 +578,6 @@
             })
             .disposed(by: disposeBag)
     }
-
-    private func subscribeConversationServiceTypingIndicator() {
-        self.typingIndicator
-            .subscribe(onNext: { [weak self] (typing) in
-                if typing {
-                    self?.addComposingIndicatorMsg()
-                } else {
-                    self?.removeComposingIndicatorMsg()
-                }
-            })
-            .disposed(by: self.disposeBag)
-    }
 }
 
 // MARK: Location sharing
@@ -812,3 +714,6 @@
         lhs.conversation == rhs.conversation
     }
 }
+
+// swiftlint:enable type_body_length
+// swiftlint:enable file_length
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContactMessageVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContactMessageVM.swift
index da1ae75..72d44f2 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContactMessageVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContactMessageVM.swift
@@ -21,10 +21,15 @@
 import Foundation
 import SwiftUI
 import RxSwift
+import RxRelay
 
 class ContactMessageVM: ObservableObject, MessageAppearanceProtocol, AvatarImageObserver, NameObserver {
     @Published var avatarImage: UIImage?
-    @Published var content: String
+    @Published var content: String {
+        didSet {
+            self.observableContent.accept(content)
+        }
+    }
     @Published var borderColor: Color
     @Published var backgroundColor: Color
     var disposeBag = DisposeBag()
@@ -33,6 +38,7 @@
     var inset: CGFloat
     var height: CGFloat
     var styling: MessageStyling = MessageStyling()
+    var observableContent = BehaviorRelay<String>(value: "")
 
     var message: MessageModel
     var username = "" {
@@ -49,6 +55,7 @@
         self.height = message.type == .initial ? 25 : 45
         self.borderColor = message.type == .initial ? Color(UIColor.clear) : Color(UIColor.secondaryLabel)
         self.content = message.content
+        self.observableContent.accept(message.content)
         if message.type != .initial {
             self.styling.textFont = self.styling.secondaryFont
             self.styling.textColor = self.styling.defaultSecondaryTextColor
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
index 623ee09..ad42356 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagePanelVM.swift
@@ -36,8 +36,11 @@
 
     let disposeBag = DisposeBag()
 
-    init(messagePanelState: PublishSubject<State>, bestName: Observable<String>) {
+    init(messagePanelState: PublishSubject<State>) {
         self.messagePanelState = messagePanelState
+    }
+
+    func subscribeBestName(bestName: Observable<String>) {
         bestName
             .share()
             .asObservable()
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
index 866d1d1..397dfc5 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
@@ -117,11 +117,16 @@
     @Published var shouldShowMap: Bool = false
     @Published var coordinates = [LocationSharingAnnotation]()
     @Published var locationSharingiewModel: LocationSharingViewModel = LocationSharingViewModel()
+    @Published var isTemporary: Bool = false
+    @Published var name: String = ""
     private let log = SwiftyBeaver.self
     var contactAvatar: UIImage = UIImage()
     var currentAccountAvatar: UIImage = UIImage()
     var myContactsLocation: CLLocationCoordinate2D?
     var myCoordinate: CLLocationCoordinate2D?
+    // jams
+    var jamsAvatarData: Data?
+    var jamsName: String = ""
 
     var accountService: AccountsService
     var profileService: ProfilesService
@@ -130,6 +135,7 @@
     var conversationService: ConversationsService
     var contactsService: ContactsService
     var nameService: NameService
+    var requestsService: RequestsService
     var transferHelper: TransferHelper
     var messagePanel: MessagePanelVM
 
@@ -144,6 +150,10 @@
         return self.messagePanelStateSubject.asObservable()
     }()
 
+    var lastMessage = BehaviorRelay<String>(value: "")
+    var lastMessageDate = BehaviorRelay<String>(value: "")
+    var lastMessageDisposeBag = DisposeBag()
+
     var hideNavigationBar = BehaviorRelay(value: false)
     let disposeBag = DisposeBag()
     var messagesDisposeBag = DisposeBag()
@@ -173,7 +183,7 @@
             }
         }
     }
-    var conversation: ConversationModel {
+    var conversation: ConversationModel! {
         didSet {
             messagesDisposeBag = DisposeBag()
             conversation.newMessages.share()
@@ -219,12 +229,6 @@
                 }
                 .disposed(by: self.messagesDisposeBag)
             self.updateLastDisplayed()
-        }
-    }
-
-    init (injectionBag: InjectionBag, conversation: ConversationModel, transferHelper: TransferHelper, bestName: Observable<String>, screenTapped: Observable<Bool>) {
-        defer {
-            self.conversation = conversation
             self.subscribeSwarmPreferences()
             self.updateColorPreference()
             self.subscribeUserAvatarForLocationSharing()
@@ -233,6 +237,10 @@
             self.subscribeMessageUpdates()
             self.subscribeMessagesActions()
         }
+    }
+
+    init (injectionBag: InjectionBag, transferHelper: TransferHelper) {
+        self.requestsService = injectionBag.requestsService
         self.conversation = ConversationModel()
         self.accountService = injectionBag.accountService
         self.profileService = injectionBag.profileService
@@ -242,8 +250,11 @@
         self.nameService = injectionBag.nameService
         self.transferHelper = transferHelper
         self.locationSharingService = injectionBag.locationSharingService
-        self.messagePanel = MessagePanelVM(messagePanelState: self.messagePanelStateSubject, bestName: bestName)
+        self.messagePanel = MessagePanelVM(messagePanelState: self.messagePanelStateSubject)
         self.subscribeLocationEvents()
+    }
+
+    func subscribeScreenTapped(screenTapped: Observable<Bool>) {
         screenTapped
             .subscribe(onNext: { [weak self] event in
                 guard let self = self else { return }
@@ -252,6 +263,31 @@
             .disposed(by: self.disposeBag)
     }
 
+    func subscribeBestName(bestName: Observable<String>) {
+        self.messagePanel.subscribeBestName(bestName: bestName)
+    }
+
+    func sendRequest() {
+        guard let conversation = self.conversation,
+              let jamiId = conversation.getParticipants().first?.jamiId else { return }
+        var avatar: String?
+        if let avatarData = self.jamsAvatarData {
+            avatar = String(data: avatarData, encoding: .utf8)
+        }
+        self.requestsService
+            .sendContactRequest(to: jamiId,
+                                withAccountId: conversation.accountId,
+                                avatar: avatar,
+                                alias: jamsName)
+            .subscribe(onCompleted: { [weak self] in
+                self?.isTemporary = false
+                self?.log.info("contact request sent")
+            }, onError: { [weak self] (_) in
+                self?.log.error("error sending contact request")
+            })
+            .disposed(by: self.disposeBag)
+    }
+
     func receiveReply(newMessage: MessageContainerModel, fromHistory: Bool) {
         let replyId = newMessage.message.reply
         if let replyContentTarget = self.getReplyContentTarget(for: replyId) {
@@ -443,7 +479,11 @@
             messageModel.message.id == newMessage.id
         }) { return false }
         let isHistory = newMessage.isReply()
-        let container = MessageContainerModel(message: newMessage, contextMenuState: self.contextStateSubject, isHistory: isHistory, localJamiId: localJamiId, preferencesColor: self.conversation.preferences.getColor())
+        let container =
+            MessageContainerModel(message: newMessage,
+                                  contextMenuState: self.contextStateSubject,
+                                  isHistory: isHistory,
+                                  localJamiId: localJamiId, preferencesColor: self.conversation.preferences.getColor())
         self.subscribeMessage(container: container)
         if fromHistory {
             self.messagesModels.append(container)
@@ -455,6 +495,24 @@
         if newMessage.isReply() {
             self.receiveReply(newMessage: container, fromHistory: fromHistory)
         }
+
+        if self.messagesModels.count > 1 && fromHistory {
+            return true
+        }
+        lastMessageDisposeBag = DisposeBag()
+        self.lastMessageDate.accept(newMessage.receivedDate.conversationTimestamp())
+        if newMessage.type != .contact {
+            self.lastMessage.accept(newMessage.content)
+        } else {
+            container.contactViewModel.observableContent
+                .startWith( container.contactViewModel.observableContent.value)
+                .subscribe { [weak self] content in
+                    guard let self = self else { return }
+                    self.lastMessage.accept(content)
+                } onError: { _ in
+                }
+                .disposed(by: lastMessageDisposeBag)
+        }
         return true
     }
 
@@ -566,6 +624,7 @@
         .disposed(by: container.disposeBag)
         container.listenerForInfoStateAdded()
     }
+    // swiftlint:enable cyclomatic_complexity
 
     private func subscribeSwarmPreferences() {
         self.conversationService
@@ -1024,3 +1083,4 @@
         }
     }
 }
+// swiftlint:enable type_body_length
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/TransferHelper.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/TransferHelper.swift
index 328f2d5..103dc26 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/TransferHelper.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/TransferHelper.swift
@@ -34,12 +34,30 @@
 }
 
 class TransferHelper {
+    let injectionBag: InjectionBag
     let dataTransferService: DataTransferService
-    let conversationViewModel: ConversationViewModel
 
-    init (dataTransferService: DataTransferService, conversationViewModel: ConversationViewModel) {
-        self.dataTransferService = dataTransferService
-        self.conversationViewModel = conversationViewModel
+    private var players = [String: PlayerViewModel]()
+
+    func getPlayer(messageID: String) -> PlayerViewModel? {
+        return players[messageID]
+    }
+
+    func setPlayer(messageID: String, player: PlayerViewModel) { players[messageID] = player }
+
+    func closeAllPlayers() {
+        let queue = DispatchQueue.global(qos: .default)
+        queue.sync {
+            self.players.values.forEach { (player) in
+                player.closePlayer()
+            }
+            self.players.removeAll()
+        }
+    }
+
+    init (injectionBag: InjectionBag) {
+        self.dataTransferService = injectionBag.dataTransferService
+        self.injectionBag = injectionBag
     }
 
     func acceptTransfer(conversation: ConversationModel, message: MessageModel) -> NSDataTransferError {
@@ -99,7 +117,7 @@
             return nil
         }
 
-        if let playerModel = conversationViewModel.getPlayer(messageID: String(message.id)) {
+        if let playerModel = self.getPlayer(messageID: String(message.id)) {
             return playerModel
         }
         let transferInfo = self.getTransferFileData(content: message.content)
@@ -117,8 +135,8 @@
                 if pathString.isEmpty {
                     return nil
                 }
-                let model = PlayerViewModel(injectionBag: conversationViewModel.injectionBag, path: pathString)
-                conversationViewModel.setPlayer(messageID: String(message.id), player: model)
+                let model = PlayerViewModel(injectionBag: self.injectionBag, path: pathString)
+                self.setPlayer(messageID: String(message.id), player: model)
                 return model
             }
             // first search for incoming video in downloads folder and for outgoing in recorded
@@ -143,8 +161,8 @@
                     return nil
                 }
             }
-            let model = PlayerViewModel(injectionBag: conversationViewModel.injectionBag, path: pathString)
-            conversationViewModel.setPlayer(messageID: String(message.id), player: model)
+            let model = PlayerViewModel(injectionBag: self.injectionBag, path: pathString)
+            self.setPlayer(messageID: String(message.id), player: model)
             return model
         }
         return nil
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
index 75bebfa..f1e0bb4 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
@@ -46,7 +46,7 @@
 }
 
 struct MessagesListView: View {
-    @StateObject var model: MessagesListVM
+    @ObservedObject var model: MessagesListVM
     @SwiftUI.State var showScrollToLatestButton = false
     let scrollReserved = UIScreen.main.bounds.height * 1.5
 
@@ -125,6 +125,9 @@
                         }
                 }
             }
+            if model.isTemporary {
+                temporaryConversationView()
+            }
         }
         .onChange(of: model.screenTapped, perform: { _ in
             /* We cannot use SwiftUI's onTapGesture here because it would
@@ -268,6 +271,36 @@
         .shadowForConversation()
     }
 
+    func temporaryConversationView() -> some View {
+        ZStack {
+            Color(UIColor.systemBackground).edgesIgnoringSafeArea(.all)
+            VStack {
+                VStack {
+                    Text(model.name + " " + L10n.Conversation.notContactLabel)
+                        .frame(maxWidth: .infinity)
+                        .multilineTextAlignment(.center)
+                    Text(L10n.Conversation.addToContactsLabel)
+                }
+                .padding()
+                .background(Color(UIColor.secondarySystemBackground))
+                Spacer()
+                Button(action: {
+                    model.sendRequest()
+                }) {
+                    Text(L10n.Conversation.addToContactsButton)
+                        .padding()
+                        .frame(maxWidth: .infinity)
+                        .background(Color.green)
+                        .foregroundColor(.white)
+                        .cornerRadius(12)
+                        .padding(.horizontal)
+                }
+                Spacer()
+                    .frame(height: 20)
+            }
+        }
+    }
+
     private func hideKeyboardIfNeed() {
         if keyboardHeight > 0 {
             withAnimation {
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
deleted file mode 100644
index 4c7d502..0000000
--- a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
- *
- *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
- *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
- *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
- *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
- *
- *  This program is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 3 of the License, or
- *  (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  You should have received a copy of the GNU General Public License
- *  along with this program; if not, write to the Free Software
- *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
- */
-
-import RxSwift
-import RxCocoa
-import SwiftyBeaver
-import MobileCoreServices
-
-enum BubblePosition {
-    case received
-    case sent
-    case generated
-}
-
-enum MessageSequencing {
-    case singleMessage
-    case firstOfSequence
-    case lastOfSequence
-    case middleOfSequence
-    case unknown
-}
-
-enum GeneratedMessageType: String {
-    case receivedContactRequest = "Contact request received"
-    case contactAdded = "Contact added"
-    case missedIncomingCall = "Missed incoming call"
-    case missedOutgoingCall = "Missed outgoing call"
-    case incomingCall = "Incoming call"
-    case outgoingCall = "Outgoing call"
-}
-
-class MessageViewModel {
-
-    private let log = SwiftyBeaver.self
-
-    private let accountService: AccountsService
-    private let conversationsService: ConversationsService
-    private let dataTransferService: DataTransferService
-    private let profileService: ProfilesService
-    var message: MessageModel
-    var metaData: LPLinkMetadata?
-
-    var profileImageData = BehaviorRelay<Data?>(value: nil)
-
-    var shouldShowTimeString: Bool = false
-    lazy var timeStringShown: String = { [weak self] in
-        guard let self = self else { return "" }
-        return MessageViewModel.getTimeLabelString(forTime: self.receivedDate)
-    }()
-
-    var sequencing: MessageSequencing = .unknown
-    var isComposingIndicator: Bool = false
-
-    var isText: Bool { return self.message.type == .text }
-
-    private let disposeBag = DisposeBag()
-    let injectBug: InjectionBag
-
-    init(withInjectionBag injectionBag: InjectionBag,
-         withMessage message: MessageModel, isLastDisplayed: Bool) {
-        self.accountService = injectionBag.accountService
-        self.conversationsService = injectionBag.conversationsService
-        self.dataTransferService = injectionBag.dataTransferService
-        self.profileService = injectionBag.profileService
-        self.injectBug = injectionBag
-        self.message = message
-        self.initialTransferStatus = message.transferStatus
-        self.status.onNext(message.status)
-        self.displayReadIndicator = BehaviorRelay<Bool>(value: isLastDisplayed)
-        // self.displayReadIndicator.accept(isLastDisplayed)
-        self.subscribeProfileServiceContactPhoto()
-
-        if isTransfer {
-            self.conversationsService
-                .sharedResponseStream
-                .filter({ [weak self] (transferEvent) in
-                    guard let transferId: String = transferEvent.getEventInput(ServiceEventInput.transferId) else { return false }
-                    return  transferEvent.eventType == ServiceEventType.dataTransferMessageUpdated &&
-                        self?.daemonId == transferId
-                })
-                .subscribe(onNext: { [weak self] transferEvent in
-                    guard let transferId: String = transferEvent.getEventInput(ServiceEventInput.transferId),
-                          let transferStatus: DataTransferStatus = transferEvent.getEventInput(ServiceEventInput.state) else {
-                        return
-                    }
-                    self?.log.debug("MessageViewModel: dataTransferMessageUpdated - id:\(transferId) status:\(transferStatus)")
-                    self?.message.transferStatus = transferStatus
-                    self?.transferStatus.onNext(transferStatus)
-                })
-                .disposed(by: disposeBag)
-        } else {
-            // subscribe to message status updates for outgoing messages
-            self.conversationsService
-                .sharedResponseStream
-                .filter({ [weak self] messageUpdateEvent in
-                    let event = messageUpdateEvent.eventType == ServiceEventType.lastDisplayedMessageUpdated
-                    let message = messageUpdateEvent
-                        .getEventInput(.oldDisplayedMessage) == self?.message.id ||
-                        messageUpdateEvent
-                        .getEventInput(.newDisplayedMessage) == self?.message.id
-                    return event && message
-                })
-                .subscribe(onNext: { [weak self] messageUpdateEvent in
-                    if let oldMessage: String = messageUpdateEvent.getEventInput(.oldDisplayedMessage),
-                       oldMessage == self?.message.id {
-                        print("@@@@@last displayed removed", message.id)
-                        self?.displayReadIndicator.accept(false)
-                    } else if let newMessage: String = messageUpdateEvent.getEventInput(.newDisplayedMessage),
-                              newMessage == self?.message.id {
-                        print("@@@@@last displayed added", message.id)
-                        self?.displayReadIndicator.accept(true)
-                    }
-                })
-                .disposed(by: self.disposeBag)
-        }
-    }
-
-    private func subscribeProfileServiceContactPhoto() {
-        guard let account = self.accountService.currentAccount else { return }
-        let schema: URIType = account.type == .sip ? .sip : .ring
-        guard let contactURI = JamiURI(schema: schema, infoHash: self.message.authorId).uriString else { return }
-        self.profileService
-            .getProfile(uri: contactURI,
-                        createIfNotexists: false,
-                        accountId: account.id)
-            .subscribe(onNext: { [weak self] profile in
-                if let photo = profile.photo,
-                   let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
-                    self?.profileImageData.accept(data)
-                }
-            })
-            .disposed(by: disposeBag)
-    }
-
-    var content: String {
-        return self.message.content
-    }
-
-    var receivedDate: Date {
-        return self.message.receivedDate
-    }
-
-    var daemonId: String {
-        return self.message.daemonId
-    }
-
-    var messageId: String {
-        return self.message.id
-    }
-
-    var isTransfer: Bool {
-        return self.message.type == .fileTransfer
-    }
-
-    var shouldDisplayTransferedImage: Bool {
-        if !self.isTransfer {
-            return false
-        }
-        if !self.message.incoming &&
-            (   self.message.transferStatus != .error ||
-                    self.message.transferStatus != .canceled) {
-            return true
-        }
-
-        if self.message.transferStatus == .success {
-            return true
-        }
-
-        return false
-    }
-
-    var status = BehaviorSubject<MessageStatus>(value: .unknown)
-    let displayReadIndicator: BehaviorRelay<Bool>
-
-    var transferStatus = BehaviorSubject<DataTransferStatus>(value: .unknown)
-    var lastTransferStatus: DataTransferStatus = .unknown
-    var initialTransferStatus: DataTransferStatus
-
-    func bubblePosition() -> BubblePosition {
-        if self.message.type == .call || self.message.type == .contact {
-            return .generated
-        }
-        if self.message.incoming {
-            return .received
-        } else {
-            return .sent
-        }
-    }
-
-    typealias TransferParsingTuple = (fileName: String, fileSize: String?, identifier: String?)
-
-    var transferFileData: TransferParsingTuple {
-        let contentArr = self.content.components(separatedBy: "\n")
-        var name: String
-        var identifier: String?
-        var size: String?
-        if contentArr.count > 2 {
-            name = contentArr[0]
-            size = contentArr[1]
-            identifier = contentArr[2]
-        } else if contentArr.count > 1 {
-            name = contentArr[0]
-            size = contentArr[1]
-        } else {
-            name = content
-        }
-        return (name, size, identifier)
-    }
-
-    func getURLFromPhotoLibrary(conversationID: String, completionHandler: @escaping (URL?) -> Void) -> Bool {
-        return false
-    }
-
-    func removeFile(conversationID: String, accountId: String, isSwarm: Bool) {
-        guard let url = self.transferedFile(conversationID: conversationID, accountId: accountId, isSwarm: isSwarm) else { return }
-        self.dataTransferService.removeFile(at: url)
-    }
-
-    func transferedFile(conversationID: String, accountId: String, isSwarm: Bool) -> URL? {
-        if self.lastTransferStatus != .success &&
-            self.message.transferStatus != .success {
-            return nil
-        }
-        let transferInfo = transferFileData
-        if isSwarm {
-            return self.dataTransferService.getFileUrlForSwarm(fileName: self.message.daemonId, accountID: accountId, conversationID: conversationID)
-        }
-        if self.message.incoming {
-            return self.dataTransferService
-                .getFileUrlNonSwarm(fileName: transferInfo.fileName,
-                                    inFolder: Directories.downloads.rawValue,
-                                    accountID: accountId,
-                                    conversationID: conversationID)
-        }
-
-        let recorded = self.dataTransferService
-            .getFileUrlNonSwarm(fileName: transferInfo.fileName,
-                                inFolder: Directories.recorded.rawValue,
-                                accountID: accountId,
-                                conversationID: conversationID)
-        guard recorded == nil, recorded?.path.isEmpty ?? true else { return recorded }
-        return self.dataTransferService
-            .getFileUrlNonSwarm(fileName: transferInfo.fileName,
-                                inFolder: Directories.downloads.rawValue,
-                                accountID: accountId,
-                                conversationID: conversationID)
-    }
-
-    func getPlayer(conversationViewModel: ConversationViewModel) -> PlayerViewModel? {
-        if self.lastTransferStatus != .success &&
-            self.message.transferStatus != .success {
-            return nil
-        }
-
-        if let playerModel = conversationViewModel.getPlayer(messageID: String(self.messageId)) {
-            return playerModel
-        }
-        let transferInfo = transferFileData
-        let name = !conversationViewModel.conversation.isSwarm() ? transferInfo.fileName : self.message.daemonId
-        guard let fileExtension = NSURL(fileURLWithPath: name).pathExtension else {
-            return nil
-        }
-        if fileExtension.isMediaExtension() {
-            if conversationViewModel.conversation.isSwarm() {
-                let path = self.dataTransferService
-                    .getFileUrlForSwarm(fileName: self.message.daemonId,
-                                        accountID: conversationViewModel.conversation.accountId,
-                                        conversationID: conversationViewModel.conversation.id)
-                let pathString = path?.path ?? ""
-                if pathString.isEmpty {
-                    return nil
-                }
-                let model = PlayerViewModel(injectionBag: injectBug, path: pathString)
-                conversationViewModel.setPlayer(messageID: String(self.messageId), player: model)
-                return model
-            }
-            // first search for incoming video in downloads folder and for outgoing in recorded
-            let folderName = self.message.incoming ? Directories.downloads.rawValue : Directories.recorded.rawValue
-            var path = self.dataTransferService
-                .getFileUrlNonSwarm(fileName: name,
-                                    inFolder: folderName,
-                                    accountID: conversationViewModel.conversation.accountId,
-                                    conversationID: conversationViewModel.conversation.id)
-            var pathString = path?.path ?? ""
-            if pathString.isEmpty && self.message.incoming {
-                return nil
-            } else if pathString.isEmpty {
-                // try to search outgoing video in downloads folder
-                path = self.dataTransferService
-                    .getFileUrlNonSwarm(fileName: name,
-                                        inFolder: Directories.downloads.rawValue,
-                                        accountID: conversationViewModel.conversation.accountId,
-                                        conversationID: conversationViewModel.conversation.id)
-                pathString = path?.path ?? ""
-                if pathString.isEmpty {
-                    return nil
-                }
-            }
-            let model = PlayerViewModel(injectionBag: injectBug, path: pathString)
-            conversationViewModel.setPlayer(messageID: String(self.messageId), player: model)
-            return model
-        }
-        return nil
-    }
-
-    func getTransferedImage(maxSize: CGFloat,
-                            conversationID: String,
-                            accountId: String,
-                            isSwarm: Bool) -> URL? {
-        guard let account = self.accountService
-                .getAccount(fromAccountId: accountId) else { return nil }
-        if self.message.incoming &&
-            self.lastTransferStatus != .success &&
-            self.message.transferStatus != .success {
-            return nil
-        }
-        let transferInfo = transferFileData
-        let name = isSwarm ? self.message.daemonId : transferInfo.fileName
-        return self.dataTransferService
-            .getFileUrlForSwarm(fileName: name, accountID: account.id, conversationID: conversationID)
-    }
-
-    private static func getTimeLabelString(forTime time: Date) -> String {
-        // get the current time
-        let currentDateTime = Date()
-
-        // prepare formatter
-        let dateFormatter = DateFormatter()
-
-        if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame {
-            // age: [0, received the previous day[
-            dateFormatter.dateFormat = "h:mma"
-        } else if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame {
-            // age: [received the previous day, received 7 days ago[
-            dateFormatter.dateFormat = "E h:mma"
-        } else if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame {
-            // age: [received 7 days ago, received the previous year[
-            dateFormatter.dateFormat = "MMM d, h:mma"
-        } else {
-            // age: [received the previous year, inf[
-            dateFormatter.dateFormat = "MMM d, yyyy h:mma"
-        }
-
-        // generate the string containing the message time
-        return dateFormatter.string(from: time).uppercased()
-    }
-}
diff --git a/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift
index b773086..4b83da3 100644
--- a/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift
+++ b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift
@@ -308,12 +308,6 @@
         }
         if let model = getConversationViewModelForId(conversationId: conversationId) {
             self.showConversation(withConversationViewModel: model)
-        } else if let request = self.requestsService.getRequest(withId: conversationId, accountId: accountId) {
-            let conversationViewModel = ConversationViewModel(with: self.injectionBag)
-            let conversation = ConversationModel(request: request)
-            conversationViewModel.conversation =  conversation
-            conversationViewModel.request = request
-            self.showConversation(withConversationViewModel: conversationViewModel)
         }
         if !shouldOpenSmarList {
             let viewControllers = navigationViewController.viewControllers
@@ -351,14 +345,8 @@
             smartListViewController = smartViewController
             return
         }
+        //        let smartViewController = SwarmCreationViewController.instantiate(with: self.injectionBag)
         let smartViewController = SmartlistViewController.instantiate(with: self.injectionBag)
-        let contactRequestsViewController = ContactRequestsViewController.instantiate(with: self.injectionBag)
-        contactRequestsViewController.viewModel.state.take(until: contactRequestsViewController.rx.deallocated)
-            .subscribe(onNext: { [weak self] (state) in
-                self?.stateSubject.onNext(state)
-            })
-            .disposed(by: self.disposeBag)
-        smartViewController.addContactRequestVC(controller: contactRequestsViewController)
         self.present(viewController: smartViewController, withStyle: .show, withAnimation: true, withStateable: smartViewController.viewModel)
         smartListViewController = smartViewController
     }
@@ -371,8 +359,8 @@
         let viewControllers = self.navigationViewController.children
         for controller in viewControllers {
             if let smartController = controller as? SmartlistViewController {
-                for model in smartController.viewModel.conversationViewModels where
-                model.conversation.isCoredialog() && model.conversation.getParticipants().first?.jamiId == jamiId {
+                for model in smartController.viewModel.conversationsModel.conversationViewModels where
+                    model.conversation.isCoredialog() && model.conversation.getParticipants().first?.jamiId == jamiId {
                     return model
                 }
             }
@@ -384,8 +372,8 @@
         let viewControllers = self.navigationViewController.children
         for controller in viewControllers {
             if let smartController = controller as? SmartlistViewController {
-                for model in smartController.viewModel.conversationViewModels where
-                model.conversation.id == conversationId {
+                for model in smartController.viewModel.conversationsModel.conversationViewModels where
+                    model.conversation.id == conversationId {
                     return model
                 }
             }
@@ -398,3 +386,5 @@
         presentingVC[VCType.conversation.rawValue] = false
     }
 }
+// swiftlint:enable cyclomatic_complexity
+// swiftlint:enable type_body_length
diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift
index 70e674d..137404a 100644
--- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift
@@ -109,21 +109,6 @@
             })
             .disposed(by: self.disposeBag)
 
-        // unread messages
-        if let unreadMessages = self.newMessagesLabel {
-            item.unreadMessages
-                .observe(on: MainScheduler.instance)
-                .startWith(item.unreadMessages.value)
-                .bind(to: unreadMessages.rx.text)
-                .disposed(by: self.disposeBag)
-        }
-        if let unreadMessagesIndicator = self.newMessagesIndicator {
-            item.hideNewMessagesLabel
-                .observe(on: MainScheduler.instance)
-                .startWith(item.hideNewMessagesLabel.value)
-                .bind(to: unreadMessagesIndicator.rx.isHidden)
-                .disposed(by: self.disposeBag)
-        }
         // presence
         if self.presenceIndicator != nil {
             item.contactPresence.asObservable()
@@ -147,21 +132,13 @@
             .bind(to: self.nameLabel.rx.text)
             .disposed(by: self.disposeBag)
         self.nameLabel.lineBreakMode = .byTruncatingTail
-        // last message date
-        if let lastMessageTime = self.lastMessageDateLabel {
-            item.lastMessageReceivedDate
-                .observe(on: MainScheduler.instance)
-                .startWith(item.lastMessageReceivedDate.value)
-                .bind(to: lastMessageTime.rx.text)
-                .disposed(by: self.disposeBag)
-        }
 
         // last message preview
         if let lastMessage = self.lastMessagePreviewLabel {
             lastMessage.lineBreakMode = .byTruncatingTail
-            item.lastMessage
+            item.lastMessageObservable
                 .observe(on: MainScheduler.instance)
-                .startWith(item.lastMessage.value)
+                .startWith(item.lastMessage)
                 .bind(to: lastMessage.rx.text)
                 .disposed(by: self.disposeBag)
         }
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
index 7125b54..055507d 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
@@ -4,7 +4,6 @@
     <dependencies>
         <deployment identifier="iOS"/>
         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
-        <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -16,287 +15,21 @@
                 <viewController id="Raw-Ee-7sK" customClass="SmartlistViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2dZ-8A-4nq">
                         <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
-                        <autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
-                        <subviews>
-                            <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="Rda-vE-7T5">
-                                <rect key="frame" x="0.0" y="20" width="375" height="647"/>
-                                <subviews>
-                                    <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="searching" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sy4-4D-ah4">
-                                        <rect key="frame" x="0.0" y="-40" width="375" height="40"/>
-                                        <constraints>
-                                            <constraint firstAttribute="height" constant="40" id="4tZ-iV-gfE"/>
-                                        </constraints>
-                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                        <nil key="textColor"/>
-                                        <nil key="highlightedColor"/>
-                                    </label>
-                                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fK7-Ou-K2i">
-                                        <rect key="frame" x="0.0" y="0.0" width="375" height="58.5"/>
-                                        <subviews>
-                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="goP-9T-lJ1">
-                                                <rect key="frame" x="10" y="10" width="355" height="38.5"/>
-                                                <subviews>
-                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="3qc-Hq-jRW">
-                                                        <rect key="frame" x="0.0" y="0.0" width="320" height="38.5"/>
-                                                        <subviews>
-                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="749" text="No network connectivity" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gFu-6Z-dl9">
-                                                                <rect key="frame" x="67.5" y="0.0" width="185" height="20.5"/>
-                                                                <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
-                                                                <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                                                <nil key="highlightedColor"/>
-                                                            </label>
-                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Be sure cellular access is granted on your settings" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wJ3-e7-CMi">
-                                                                <rect key="frame" x="8" y="22.5" width="304" height="16"/>
-                                                                <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
-                                                                <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                                                <nil key="highlightedColor"/>
-                                                            </label>
-                                                        </subviews>
-                                                        <viewLayoutGuide key="safeArea" id="7kw-IK-33Z"/>
-                                                    </stackView>
-                                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aUA-Rj-5gi">
-                                                        <rect key="frame" x="320" y="0.0" width="35" height="38.5"/>
-                                                        <constraints>
-                                                            <constraint firstAttribute="width" constant="35" id="M1l-Hd-D5Z"/>
-                                                        </constraints>
-                                                        <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                                        <state key="normal" title="Button"/>
-                                                        <buttonConfiguration key="configuration" style="plain" image="gearshape" catalog="system"/>
-                                                    </button>
-                                                </subviews>
-                                                <viewLayoutGuide key="safeArea" id="QVu-d0-XQT"/>
-                                                <constraints>
-                                                    <constraint firstAttribute="height" constant="38.5" id="DvO-3i-ryD"/>
-                                                </constraints>
-                                            </stackView>
-                                        </subviews>
-                                        <viewLayoutGuide key="safeArea" id="yx6-GK-nm1"/>
-                                        <color key="backgroundColor" red="0.89803922179999995" green="0.54509806630000002" blue="0.52549022440000004" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
-                                        <constraints>
-                                            <constraint firstItem="goP-9T-lJ1" firstAttribute="leading" secondItem="fK7-Ou-K2i" secondAttribute="leading" constant="10" id="hTx-27-IZX"/>
-                                            <constraint firstAttribute="trailing" secondItem="goP-9T-lJ1" secondAttribute="trailing" constant="10" id="hnI-Kq-jjD"/>
-                                            <constraint firstAttribute="bottom" secondItem="goP-9T-lJ1" secondAttribute="bottom" constant="10" id="qbw-52-D1n"/>
-                                            <constraint firstItem="goP-9T-lJ1" firstAttribute="top" secondItem="fK7-Ou-K2i" secondAttribute="top" constant="10" id="qmt-fC-8L0"/>
-                                        </constraints>
-                                    </view>
-                                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="muN-Wj-ggZ">
-                                        <rect key="frame" x="0.0" y="58.5" width="375" height="100"/>
-                                        <subviews>
-                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LfN-ON-tfu">
-                                                <rect key="frame" x="15" y="0.0" width="345" height="85"/>
-                                                <subviews>
-                                                    <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="D3q-LJ-usz">
-                                                        <rect key="frame" x="6" y="6" width="333" height="75"/>
-                                                        <subviews>
-                                                            <stackView opaque="NO" contentMode="scaleToFill" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="uSX-OA-C81">
-                                                                <rect key="frame" x="0.0" y="0.0" width="333" height="75"/>
-                                                                <subviews>
-                                                                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="donation" translatesAutoresizingMaskIntoConstraints="NO" id="Acg-jm-Z3h">
-                                                                        <rect key="frame" x="0.0" y="0.0" width="60" height="75"/>
-                                                                        <constraints>
-                                                                            <constraint firstAttribute="height" constant="75" id="FA4-KN-Xib"/>
-                                                                            <constraint firstAttribute="width" constant="60" id="STa-ce-tka"/>
-                                                                        </constraints>
-                                                                        <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="default" weight="regular"/>
-                                                                    </imageView>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="top" translatesAutoresizingMaskIntoConstraints="NO" id="E6U-Dl-DvQ">
-                                                                        <rect key="frame" x="60" y="0.0" width="273" height="75"/>
-                                                                        <subviews>
-                                                                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="UJA-F7-jD1">
-                                                                                <rect key="frame" x="0.0" y="0.0" width="273" height="45"/>
-                                                                                <subviews>
-                                                                                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ViS-TN-KCr">
-                                                                                        <rect key="frame" x="0.0" y="0.0" width="12" height="45"/>
-                                                                                        <constraints>
-                                                                                            <constraint firstAttribute="width" constant="12" id="gxG-cu-hKy"/>
-                                                                                        </constraints>
-                                                                                    </view>
-                                                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="If you enjoy using Jami and believe in our mission, would you make a donation?" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="9" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zuu-Bz-eqP">
-                                                                                        <rect key="frame" x="12" y="0.0" width="261" height="45"/>
-                                                                                        <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
-                                                                                        <nil key="textColor"/>
-                                                                                        <nil key="highlightedColor"/>
-                                                                                    </label>
-                                                                                </subviews>
-                                                                                <constraints>
-                                                                                    <constraint firstAttribute="trailing" secondItem="zuu-Bz-eqP" secondAttribute="trailing" id="h5D-yE-blJ"/>
-                                                                                </constraints>
-                                                                            </stackView>
-                                                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="equalCentering" translatesAutoresizingMaskIntoConstraints="NO" id="rLw-rO-feu">
-                                                                                <rect key="frame" x="0.0" y="45" width="146" height="30"/>
-                                                                                <subviews>
-                                                                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nki-qc-gWk">
-                                                                                        <rect key="frame" x="0.0" y="0.0" width="73" height="30"/>
-                                                                                        <state key="normal" title="Button"/>
-                                                                                        <buttonConfiguration key="configuration" style="plain">
-                                                                                            <attributedString key="attributedTitle">
-                                                                                                <fragment content="Not now">
-                                                                                                    <attributes>
-                                                                                                        <color key="NSColor" systemColor="systemGrayColor"/>
-                                                                                                        <font key="NSFont" size="14" name=".SFNS-Regular"/>
-                                                                                                    </attributes>
-                                                                                                </fragment>
-                                                                                            </attributedString>
-                                                                                        </buttonConfiguration>
-                                                                                    </button>
-                                                                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="riP-u7-388">
-                                                                                        <rect key="frame" x="81.5" y="0.0" width="64.5" height="30"/>
-                                                                                        <state key="normal" title="Button"/>
-                                                                                        <buttonConfiguration key="configuration" style="plain">
-                                                                                            <attributedString key="attributedTitle">
-                                                                                                <fragment content="Donate">
-                                                                                                    <attributes>
-                                                                                                        <color key="NSColor" name="linkColor" catalog="System" colorSpace="catalog"/>
-                                                                                                        <font key="NSFont" metaFont="system" size="14"/>
-                                                                                                    </attributes>
-                                                                                                </fragment>
-                                                                                            </attributedString>
-                                                                                        </buttonConfiguration>
-                                                                                    </button>
-                                                                                </subviews>
-                                                                            </stackView>
-                                                                        </subviews>
-                                                                        <constraints>
-                                                                            <constraint firstItem="rLw-rO-feu" firstAttribute="leading" secondItem="E6U-Dl-DvQ" secondAttribute="leading" id="PcW-DF-LFq"/>
-                                                                            <constraint firstAttribute="trailing" secondItem="UJA-F7-jD1" secondAttribute="trailing" id="gfF-09-JK8"/>
-                                                                        </constraints>
-                                                                    </stackView>
-                                                                </subviews>
-                                                            </stackView>
-                                                        </subviews>
-                                                        <constraints>
-                                                            <constraint firstItem="uSX-OA-C81" firstAttribute="top" secondItem="D3q-LJ-usz" secondAttribute="top" id="9ke-xL-HeK"/>
-                                                            <constraint firstAttribute="trailing" secondItem="uSX-OA-C81" secondAttribute="trailing" id="LFs-mk-OIK"/>
-                                                            <constraint firstItem="uSX-OA-C81" firstAttribute="leading" secondItem="D3q-LJ-usz" secondAttribute="leading" id="e4h-j9-7bj"/>
-                                                            <constraint firstAttribute="bottom" secondItem="uSX-OA-C81" secondAttribute="bottom" id="olb-7a-Qek"/>
-                                                        </constraints>
-                                                        <userDefinedRuntimeAttributes>
-                                                            <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="NO"/>
-                                                            <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
-                                                                <real key="value" value="0.0"/>
-                                                            </userDefinedRuntimeAttribute>
-                                                        </userDefinedRuntimeAttributes>
-                                                    </stackView>
-                                                </subviews>
-                                                <color key="backgroundColor" name="donationBanner"/>
-                                                <constraints>
-                                                    <constraint firstItem="D3q-LJ-usz" firstAttribute="leading" secondItem="LfN-ON-tfu" secondAttribute="leading" constant="6" id="6lR-gq-xXJ"/>
-                                                    <constraint firstAttribute="height" constant="85" id="aKY-dv-R46"/>
-                                                    <constraint firstAttribute="trailing" secondItem="D3q-LJ-usz" secondAttribute="trailing" constant="6" id="grf-wA-y1h"/>
-                                                    <constraint firstAttribute="bottom" secondItem="D3q-LJ-usz" secondAttribute="bottom" constant="4" id="lDI-FG-y9X"/>
-                                                    <constraint firstItem="D3q-LJ-usz" firstAttribute="top" secondItem="LfN-ON-tfu" secondAttribute="top" constant="6" id="lj6-6v-GhF"/>
-                                                </constraints>
-                                                <userDefinedRuntimeAttributes>
-                                                    <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
-                                                    <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
-                                                        <real key="value" value="8"/>
-                                                    </userDefinedRuntimeAttribute>
-                                                </userDefinedRuntimeAttributes>
-                                            </view>
-                                        </subviews>
-                                        <constraints>
-                                            <constraint firstAttribute="bottom" secondItem="LfN-ON-tfu" secondAttribute="bottom" constant="15" id="6ZN-gT-a7Y"/>
-                                            <constraint firstItem="LfN-ON-tfu" firstAttribute="top" secondItem="muN-Wj-ggZ" secondAttribute="top" id="A9s-8k-rSx"/>
-                                            <constraint firstItem="LfN-ON-tfu" firstAttribute="leading" secondItem="muN-Wj-ggZ" secondAttribute="leading" constant="15" id="E0T-1W-Utc"/>
-                                            <constraint firstAttribute="trailing" secondItem="LfN-ON-tfu" secondAttribute="trailing" constant="15" id="OyX-LA-Vlc"/>
-                                        </constraints>
-                                    </view>
-                                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uHv-0y-Afe">
-                                        <rect key="frame" x="0.0" y="158.5" width="375" height="488.5"/>
-                                        <subviews>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uHt-ds-l8p">
-                                                <rect key="frame" x="187.5" y="244.5" width="0.0" height="0.0"/>
-                                                <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="HFM-G6-hMN">
-                                                <rect key="frame" x="0.0" y="0.0" width="375" height="488.5"/>
-                                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                            </tableView>
-                                            <tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="opE-y7-3Rm">
-                                                <rect key="frame" x="0.0" y="0.0" width="375" height="488.5"/>
-                                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                            </tableView>
-                                        </subviews>
-                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                        <constraints>
-                                            <constraint firstItem="opE-y7-3Rm" firstAttribute="top" secondItem="uHv-0y-Afe" secondAttribute="top" id="6oJ-4c-sHy"/>
-                                            <constraint firstItem="HFM-G6-hMN" firstAttribute="top" secondItem="uHv-0y-Afe" secondAttribute="top" id="ErI-lP-Clv"/>
-                                            <constraint firstItem="uHt-ds-l8p" firstAttribute="centerX" secondItem="uHv-0y-Afe" secondAttribute="centerX" id="GzM-zr-tQ3"/>
-                                            <constraint firstAttribute="bottom" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="IZp-Zd-9TF"/>
-                                            <constraint firstItem="uHt-ds-l8p" firstAttribute="centerY" secondItem="uHv-0y-Afe" secondAttribute="centerY" id="Izd-id-gKk"/>
-                                            <constraint firstAttribute="trailing" secondItem="HFM-G6-hMN" secondAttribute="trailing" id="J7y-5I-hiu"/>
-                                            <constraint firstItem="opE-y7-3Rm" firstAttribute="leading" secondItem="uHv-0y-Afe" secondAttribute="leading" id="KHo-YY-C3y"/>
-                                            <constraint firstAttribute="bottom" secondItem="opE-y7-3Rm" secondAttribute="bottom" id="Shr-dr-27u"/>
-                                            <constraint firstItem="HFM-G6-hMN" firstAttribute="leading" secondItem="uHv-0y-Afe" secondAttribute="leading" id="SxW-MG-L7u"/>
-                                            <constraint firstAttribute="trailing" secondItem="opE-y7-3Rm" secondAttribute="trailing" id="gbS-5H-tkZ"/>
-                                        </constraints>
-                                    </view>
-                                </subviews>
-                                <constraints>
-                                    <constraint firstItem="uHv-0y-Afe" firstAttribute="top" secondItem="muN-Wj-ggZ" secondAttribute="bottom" id="0GT-be-Z1K"/>
-                                    <constraint firstAttribute="trailing" secondItem="uHv-0y-Afe" secondAttribute="trailing" id="CwS-8Z-7Bl"/>
-                                    <constraint firstItem="uHv-0y-Afe" firstAttribute="leading" secondItem="Rda-vE-7T5" secondAttribute="leading" id="VcF-tI-pwu"/>
-                                    <constraint firstItem="muN-Wj-ggZ" firstAttribute="top" secondItem="fK7-Ou-K2i" secondAttribute="bottom" id="a4H-vT-1y6"/>
-                                    <constraint firstAttribute="trailing" secondItem="muN-Wj-ggZ" secondAttribute="trailing" id="cF9-Lw-OAo"/>
-                                    <constraint firstAttribute="bottom" secondItem="uHv-0y-Afe" secondAttribute="bottom" id="dVN-YK-c9Y"/>
-                                    <constraint firstItem="muN-Wj-ggZ" firstAttribute="leading" secondItem="Rda-vE-7T5" secondAttribute="leading" id="lZe-7J-ZjC"/>
-                                    <constraint firstAttribute="bottom" secondItem="uHv-0y-Afe" secondAttribute="bottom" id="rxm-bY-9yW"/>
-                                </constraints>
-                            </stackView>
-                        </subviews>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <viewLayoutGuide key="safeArea" id="mnN-Gu-0lw"/>
                         <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                        <constraints>
-                            <constraint firstItem="Rda-vE-7T5" firstAttribute="top" secondItem="mnN-Gu-0lw" secondAttribute="top" id="8Jx-Gx-TIM"/>
-                            <constraint firstItem="Rda-vE-7T5" firstAttribute="bottom" secondItem="mnN-Gu-0lw" secondAttribute="bottom" id="duf-iA-FhS"/>
-                            <constraint firstItem="mnN-Gu-0lw" firstAttribute="trailing" secondItem="Rda-vE-7T5" secondAttribute="trailing" id="luu-Xu-KbJ"/>
-                            <constraint firstItem="Rda-vE-7T5" firstAttribute="leading" secondItem="mnN-Gu-0lw" secondAttribute="leading" id="sVq-4t-ORv"/>
-                        </constraints>
                     </view>
                     <extendedEdge key="edgesForExtendedLayout" top="YES"/>
                     <navigationItem key="navigationItem" id="zLl-0A-Dht"/>
-                    <connections>
-                        <outlet property="cellularAlertLabel" destination="wJ3-e7-CMi" id="ScO-Xe-kfT"/>
-                        <outlet property="containerView" destination="uHv-0y-Afe" id="qh0-9W-oy5"/>
-                        <outlet property="conversationsTableView" destination="HFM-G6-hMN" id="M97-IB-NUZ"/>
-                        <outlet property="disableDonationButton" destination="Nki-qc-gWk" id="AkZ-rQ-tts"/>
-                        <outlet property="donateButton" destination="riP-u7-388" id="Eel-UJ-XoZ"/>
-                        <outlet property="donationBaner" destination="muN-Wj-ggZ" id="nfE-FU-7q9"/>
-                        <outlet property="donationLabel" destination="zuu-Bz-eqP" id="zaU-I2-Crd"/>
-                        <outlet property="networkAlertLabel" destination="gFu-6Z-dl9" id="NJF-MK-pWj"/>
-                        <outlet property="networkAlertView" destination="fK7-Ou-K2i" id="amM-wV-HA8"/>
-                        <outlet property="noConversationLabel" destination="uHt-ds-l8p" id="tYK-67-lg5"/>
-                        <outlet property="searchView" destination="Y4B-5f-ij4" id="FtS-9R-atZ"/>
-                        <outlet property="settingsButton" destination="aUA-Rj-5gi" id="CeY-yv-WaH"/>
-                        <outlet property="widgetsTopConstraint" destination="8Jx-Gx-TIM" id="Zil-5y-R8j"/>
-                    </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="JSt-CJ-9Vq" userLabel="First Responder" sceneMemberID="firstResponder"/>
-                <customObject id="MBy-7P-V3E" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target"/>
-                <customObject id="Y4B-5f-ij4" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target">
-                    <connections>
-                        <outlet property="searchResultsTableView" destination="opE-y7-3Rm" id="cMo-3b-5FA"/>
-                        <outlet property="searchingLabel" destination="sy4-4D-ah4" id="lMx-Yi-EwV"/>
-                    </connections>
-                </customObject>
             </objects>
             <point key="canvasLocation" x="-108" y="-1208.5457271364319"/>
         </scene>
     </scenes>
     <resources>
-        <image name="donation" width="130" height="160"/>
-        <image name="gearshape" catalog="system" width="128" height="123"/>
-        <namedColor name="donationBanner">
-            <color red="0.83529411764705885" green="0.89411764705882346" blue="0.93725490196078431" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-        </namedColor>
         <systemColor name="systemBackgroundColor">
             <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </systemColor>
-        <systemColor name="systemGrayColor">
-            <color red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-        </systemColor>
     </resources>
 </document>
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
index e4dee30..4370c04 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2023 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2024 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -21,13 +21,8 @@
  */
 
 import UIKit
-import RxSwift
-import RxDataSources
-import RxCocoa
 import Reusable
-import SwiftyBeaver
-import ContactsUI
-import QuartzCore
+import SwiftUI
 
 // Constants
 struct SmartlistConstants {
@@ -35,737 +30,42 @@
     static let tableHeaderViewHeight: CGFloat = 30.0
 }
 
-// swiftlint:disable type_body_length
-class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased, UISearchControllerDelegate {
+class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased {
 
-    private let log = SwiftyBeaver.self
-
-    // MARK: outlets
-    @IBOutlet weak var conversationsTableView: UITableView!
-    @IBOutlet weak var containerView: UIView!
-    @IBOutlet weak var noConversationLabel: UILabel!
-    @IBOutlet weak var networkAlertLabel: UILabel!
-    @IBOutlet weak var cellularAlertLabel: UILabel!
-    @IBOutlet weak var settingsButton: UIButton!
-    @IBOutlet weak var widgetsTopConstraint: NSLayoutConstraint!
-    @IBOutlet weak var networkAlertView: UIView!
-    @IBOutlet weak var searchView: JamiSearchView!
-    @IBOutlet weak var donationBaner: UIView!
-    @IBOutlet weak var donateButton: UIButton!
-    @IBOutlet weak var disableDonationButton: UIButton!
-    @IBOutlet weak var donationLabel: UILabel!
-
-    // account selection
-    private var accounPicker = UIPickerView()
-    private let accountPickerTextView = UITextField(frame: CGRect.zero)
-    private let accountsAdapter = AccountPickerAdapter()
-    private var accountsDismissTapRecognizer: UITapGestureRecognizer!
-
-    private var selectedSegmentIndex = BehaviorRelay<Int>(value: 0)
     var viewModel: SmartlistViewModel!
-    private let disposeBag = DisposeBag()
-
-    private let contactPicker = CNContactPickerViewController()
-    private var headerView: SmartListHeaderView?
-
-    private var contactRequestVC: ContactRequestsViewController?
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        self.setupDataSources()
-        self.setupTableView()
-        self.setupUI()
-        self.applyL10n()
-        self.configureNavigationBar()
-        self.confugureAccountPicker()
-        accountsDismissTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
-        self.setupSearchBar()
-        searchView.configure(with: viewModel.injectionBag, source: viewModel, isIncognito: false, delegate: viewModel)
-        if !self.viewModel.isSipAccount() {
-            self.setUpContactRequest()
-        }
+        self.addSwiftUI()
     }
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         self.viewModel.closeAllPlayers()
         self.navigationController?.setNavigationBarHidden(false, animated: false)
-        configureCustomNavBar(usingCustomSize: true)
-        self.viewModel.updateDonationBunnerVisiblity()
+        self.navigationController?.setNavigationBarHidden(true, animated: animated)
+    }
+
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        self.navigationController?.setNavigationBarHidden(true, animated: animated)
     }
 
     override func viewWillDisappear(_ animated: Bool) {
         super.viewWillDisappear(animated)
-        configureCustomNavBar(usingCustomSize: false)
+        self.navigationController?.setNavigationBarHidden(false, animated: animated)
     }
 
-    func setupTableViewHeader(for tableView: UITableView) {
-        guard let headerView = loadHeaderView() else { return }
-
-        setupHeaderViewConstraints(headerView, in: tableView)
-        bindSegmentControlActions(headerView)
-        bindViewModelUpdates(to: headerView, in: tableView)
-    }
-
-    private func loadHeaderView() -> SmartListHeaderView? {
-        let nib = UINib(nibName: "SmartListHeaderView", bundle: nil)
-        return nib.instantiate(withOwner: nil, options: nil).first as? SmartListHeaderView
-    }
-
-    private func setupHeaderViewConstraints(_ headerView: SmartListHeaderView, in tableView: UITableView) {
-        tableView.tableHeaderView = headerView
-        NSLayoutConstraint.activate([
-            headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor, constant: -30),
-            headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor)
-        ])
-    }
-
-    private func bindSegmentControlActions(_ headerView: SmartListHeaderView) {
-        headerView.conversationsSegmentControl.addTarget(self, action: #selector(segmentAction), for: .valueChanged)
-
-        self.selectedSegmentIndex.subscribe { [weak headerView] index in
-            headerView?.conversationsSegmentControl.selectedSegmentIndex = index
-        }
-        .disposed(by: self.disposeBag)
-    }
-
-    private func bindViewModelUpdates(to headerView: SmartListHeaderView, in tableView: UITableView) {
-        self.viewModel.updateSegmentControl
-            .subscribe { [weak headerView, weak tableView, weak self] (messages, requests) in
-                guard let headerView = headerView, let tableView = tableView else { return }
-
-                let height: CGFloat = requests == 0 ? 0 : 32
-                var frame = headerView.frame
-                frame.size.height = height
-                headerView.frame = frame
-
-                headerView.setUnread(messages: messages, requests: requests)
-
-                if requests == 0 {
-                    headerView.conversationsSegmentControl.selectedSegmentIndex = 0
-                    self?.navigationItem.title = L10n.Smartlist.conversations
-                }
-
-                // Resetting the header after adjusting its frame.
-                tableView.tableHeaderView = headerView
-            }
-            .disposed(by: self.disposeBag)
-    }
-
-    @objc
-    func segmentAction(_ segmentedControl: UISegmentedControl) {
-        switch segmentedControl.selectedSegmentIndex {
-        case 0:
-            contactRequestVC?.view.isHidden = true
-            searchView.showSearchResult = true
-            self.navigationItem.title = L10n.Smartlist.conversations
-        case 1:
-            contactRequestVC?.view.isHidden = false
-            searchView.showSearchResult = false
-            self.navigationItem.title = L10n.Smartlist.invitations
-        default:
-            break
-        }
-        self.selectedSegmentIndex.accept(segmentedControl.selectedSegmentIndex)
-    }
-
-    func addContactRequestVC(controller: ContactRequestsViewController) {
-        contactRequestVC = controller
-    }
-
-    private func setUpContactRequest() {
-        guard let controller = contactRequestVC else { return }
-        addChild(controller)
-
-        // make sure that the child view controller's view is the right size
-        controller.view.frame = containerView.bounds
-        containerView.addSubview(controller.view)
-
-        // you must call this at the end per Apple's documentation
-        controller.didMove(toParent: self)
-        controller.view.isHidden = true
-        self.setupTableViewHeader(for: controller.tableView)
-        self.searchView.searchBar.rx.text.orEmpty
-            .debounce(Durations.textFieldThrottlingDuration.toTimeInterval(), scheduler: MainScheduler.instance)
-            .bind(to: (self.contactRequestVC?.viewModel.filter)!)
-            .disposed(by: disposeBag)
-    }
-
-    @objc
-    func dismissKeyboard() {
-        accountPickerTextView.resignFirstResponder()
-        view.removeGestureRecognizer(accountsDismissTapRecognizer)
-    }
-
-    private func configureCustomNavBar(usingCustomSize: Bool) {
-        guard let customNavBar = navigationController?.navigationBar as? SmartListNavigationBar else { return }
-
-        if usingCustomSize {
-            self.updateSearchBarIfActive()
-            customNavBar.usingCustomSize = true
-        } else {
-            customNavBar.removeTopView()
-            customNavBar.usingCustomSize = false
-        }
-    }
-
-    private func applyL10n() {
-        self.navigationItem.title = L10n.Smartlist.conversations
-        self.noConversationLabel.text = L10n.Smartlist.noConversation
-        self.networkAlertLabel.text = L10n.Smartlist.noNetworkConnectivity
-        self.cellularAlertLabel.text = L10n.Smartlist.cellularAccess
-
-        let attributes: [NSAttributedString.Key: Any] = [
-            .foregroundColor: UIColor.jamiButtonDark,
-            .font: UIFont.systemFont(ofSize: 14)]
-        let disableDonationTitle = NSAttributedString(string: L10n.Smartlist.disableDonation, attributes: attributes)
-        let donateTitle = NSAttributedString(string: L10n.Global.donate, attributes: attributes)
-
-        self.disableDonationButton.setAttributedTitle(disableDonationTitle, for: .normal)
-        self.donateButton.setAttributedTitle(donateTitle, for: .normal)
-        self.donationLabel.text = L10n.Smartlist.donationExplanation
-    }
-
-    private func setupUI() {
-        self.viewModel.hideNoConversationsMessage
-            .bind(to: self.noConversationLabel.rx.isHidden)
-            .disposed(by: disposeBag)
-        self.viewModel.connectionState
-            .observe(on: MainScheduler.instance)
-            .startWith(self.viewModel.networkConnectionState())
-            .subscribe(onNext: { [weak self] _ in
-                self?.updateNetworkUI()
-            })
-            .disposed(by: self.disposeBag)
-
-        self.settingsButton.backgroundColor = nil
-        self.settingsButton.setTitle("", for: .normal)
-        self.settingsButton.rx.tap
-            .subscribe(onNext: { _ in
-                if let url = URL(string: UIApplication.openSettingsURLString) {
-                    UIApplication.shared.open(url, completionHandler: nil)
-                }
-            })
-            .disposed(by: self.disposeBag)
-        self.viewModel.currentAccountChanged
-            .observe(on: MainScheduler.instance)
-            .subscribe(onNext: { [weak self] _ in
-                self?.searchBarNotActive()
-            })
-            .disposed(by: disposeBag)
-        // create account button
-        let accountButton = UIButton(type: .custom)
-        self.viewModel.profileImage.bind(to: accountButton.rx.image(for: .normal))
-            .disposed(by: disposeBag)
-        accountButton.roundedCorners = true
-        accountButton.clipsToBounds = true
-        accountButton.imageView?.contentMode = .scaleAspectFill
-        accountButton.cornerRadius = smartListAccountSize * 0.5
-        accountButton.frame = CGRect(x: 0, y: 0, width: smartListAccountSize, height: smartListAccountSize)
-        accountButton.imageEdgeInsets = UIEdgeInsets(top: -smartListAccountMargin, left: -smartListAccountMargin, bottom: -smartListAccountMargin, right: -smartListAccountMargin)
-        let accountButtonItem = UIBarButtonItem(customView: accountButton)
-        accountButtonItem
-            .customView?
-            .translatesAutoresizingMaskIntoConstraints = false
-        accountButtonItem.customView?
-            .heightAnchor
-            .constraint(equalToConstant: smartListAccountSize).isActive = true
-        accountButtonItem.customView?
-            .widthAnchor
-            .constraint(equalToConstant: smartListAccountSize).isActive = true
-        accountButton.rx.tap
-            .throttle(Durations.halfSecond.toTimeInterval(), scheduler: MainScheduler.instance)
-            .subscribe(onNext: { [weak self] in
-                self?.openAccountsList()
-            })
-            .disposed(by: self.disposeBag)
-        self.navigationItem.leftBarButtonItem = accountButtonItem
-        let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
-        space.width = 20
-        self.navigationItem.rightBarButtonItems = [createSearchButton(), space, createMenuButton()]
-        self.conversationsTableView.tableFooterView = UIView()
-        self.viewModel.donationBannerVisible
-            .observe(on: MainScheduler.instance)
-            .startWith(self.viewModel.donationBannerVisible.value)
-            .map { !$0 }
-            .bind(to: self.donationBaner.rx.isHidden)
-            .disposed(by: disposeBag)
-        self.donateButton.rx.tap
-            .subscribe(onNext: { [weak self] _ in
-                guard let self = self else { return }
-                SharedActionsPresenter.openDonationLink()
-                self.viewModel.temporaryDisableDonationCampaign()
-            })
-            .disposed(by: self.disposeBag)
-        self.disableDonationButton.rx.tap
-            .subscribe(onNext: { [weak self] _ in
-                guard let self = self else { return }
-                self.viewModel.temporaryDisableDonationCampaign()
-            })
-            .disposed(by: self.disposeBag)
-    }
-
-    private func createSearchButton() -> UIBarButtonItem {
-        let imageSettings = UIImage(systemName: "square.and.pencil") as UIImage?
-        let generalSettingsButton = UIButton(type: UIButton.ButtonType.system) as UIButton
-        generalSettingsButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
-        generalSettingsButton.setImage(imageSettings, for: .normal)
-        generalSettingsButton.tintColor = .jamiButtonDark
-        generalSettingsButton.rx.tap
-            .subscribe(onNext: { [weak self] in
-                self?.searchController.isActive = true
-            })
-            .disposed(by: self.disposeBag)
-        return UIBarButtonItem(customView: generalSettingsButton)
-    }
-
-    private func createMenuButton() -> UIBarButtonItem {
-        let imageSettings = UIImage(systemName: "ellipsis.circle") as UIImage?
-        let generalSettingsButton = UIButton(type: UIButton.ButtonType.system) as UIButton
-        generalSettingsButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
-        generalSettingsButton.setImage(imageSettings, for: .normal)
-        generalSettingsButton.menu = createMenu()
-        generalSettingsButton.tintColor = .jamiButtonDark
-        generalSettingsButton.showsMenuAsPrimaryAction = true
-        return UIBarButtonItem(customView: generalSettingsButton)
-    }
-
-    private func shareAccountInfo() {
-        guard let content = self.viewModel.accountInfoToShare else { return }
-
-        let sourceView: UIView
-        if UIDevice.current.userInterfaceIdiom == .phone {
-            sourceView = self.view
-        } else if let navigationBar = self.navigationController?.navigationBar {
-            sourceView = navigationBar
-        } else {
-            sourceView = self.view
-        }
-
-        SharedActionsPresenter.shareAccountInfo(onViewController: self, sourceView: sourceView, content: content)
-    }
-
-    func confugureAccountPicker() {
-        accountPickerTextView.inputView = accounPicker
-        view.addSubview(accountPickerTextView)
-
-        accounPicker.backgroundColor = .jamiBackgroundSecondaryColor
-        self.viewModel.accounts
-            .observe(on: MainScheduler.instance)
-            .bind(to: accounPicker.rx.items(adapter: accountsAdapter))
-            .disposed(by: disposeBag)
-        if let account = self.viewModel.currentAccount,
-           let row = accountsAdapter.rowForAccountId(account: account) {
-            accounPicker.selectRow(row, inComponent: 0, animated: true)
-        }
-        self.viewModel.currentAccountChanged
-            .observe(on: MainScheduler.instance)
-            .subscribe(onNext: { [weak self] currentAccount in
-                guard let self = self else { return }
-                if let account = currentAccount,
-                   let row = self.accountsAdapter.rowForAccountId(account: account) {
-                    self.accounPicker.selectRow(row, inComponent: 0, animated: true)
-                }
-            })
-            .disposed(by: disposeBag)
-        accounPicker.rx.modelSelected(AccountItem.self)
-            .subscribe(onNext: { [weak self] model in
-                let account = model[0].account
-                self?.viewModel.changeCurrentAccount(accountId: account.id)
-            })
-            .disposed(by: disposeBag)
-        let accountsLabel = UILabel(frame: CGRect(x: 0, y: 20, width: self.view.frame.width, height: 40))
-        accountsLabel.text = L10n.Smartlist.accountsTitle
-        accountsLabel.font = UIFont.systemFont(ofSize: 25, weight: .light)
-        accountsLabel.textColor = .jamiSecondary
-        accountsLabel.textAlignment = .center
-        let addAccountButton = UIButton(type: .custom)
-        addAccountButton.frame = CGRect(x: 0, y: 0, width: 250, height: 40)
-        addAccountButton.contentHorizontalAlignment = .right
-        addAccountButton.setTitle(L10n.Smartlist.addAccountButton, for: .normal)
-        addAccountButton.setTitleColor(.jamiButtonDark, for: .normal)
-        addAccountButton.titleLabel?.font = UIFont(name: "HelveticaNeue-Light", size: 23)
-
-        // Enable auto-shrink
-        addAccountButton.titleLabel?.adjustsFontSizeToFitWidth = true
-        addAccountButton.titleLabel?.minimumScaleFactor = 0.5 // The minimum scale factor for the font size
-        addAccountButton.sizeToFit()
-
-        let flexibleBarButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil)
-        let addBarButton = UIBarButtonItem(customView: addAccountButton)
-        let toolbar = UIToolbar()
-        toolbar.barTintColor = .jamiBackgroundSecondaryColor
-        toolbar.isTranslucent = false
-
-        toolbar.sizeToFit()
-        toolbar.center = CGPoint(x: self.view.frame.width * 0.5, y: 200)
-
-        toolbar.items = [flexibleBarButton, addBarButton]
-        accountPickerTextView.inputAccessoryView = toolbar
-        addAccountButton.rx.tap
-            .throttle(Durations.halfSecond.toTimeInterval(), scheduler: MainScheduler.instance)
-            .subscribe(onNext: { [weak self] in
-                self?.startAccountCreation()
-            })
-            .disposed(by: self.disposeBag)
-    }
-
-    func setupDataSources() {
-        // Configure cells closure for the datasources
-        let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, ConversationSection.Item)
-            -> UITableViewCell = {
-                (   _: TableViewSectionedDataSource<ConversationSection>,
-                    tableView: UITableView,
-                    indexPath: IndexPath,
-                    conversationItem: ConversationSection.Item) in
-
-                let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SmartListCell.self)
-                cell.configureFromItem(conversationItem)
-                return cell
-            }
-
-        // Create DataSources for conversations and filtered conversations
-        let conversationsDataSource = RxTableViewSectionedReloadDataSource<ConversationSection>(configureCell: configureCell)
-        // Allows to delete
-        conversationsDataSource.canEditRowAtIndexPath = { _, _  in
-            return true
-        }
-
-        // Bind TableViews to DataSources
-        self.viewModel.conversations
-            .bind(to: self.conversationsTableView.rx.items(dataSource: conversationsDataSource))
-            .disposed(by: disposeBag)
-    }
-
-    func setupTableView() {
-        // Set row height
-        self.conversationsTableView.rowHeight = SmartlistConstants.smartlistRowHeight
-
-        // Register Cell
-        self.conversationsTableView.register(cellType: SmartListCell.self)
-        // Deselect the rows
-        self.conversationsTableView.rx.itemSelected
-            .subscribe(onNext: { [weak self] indexPath in
-                self?.conversationsTableView.deselectRow(at: indexPath, animated: true)
-            })
-            .disposed(by: disposeBag)
-
-        self.conversationsTableView.rx.setDelegate(self).disposed(by: disposeBag)
-
-        // table header
-        setupTableViewHeader(for: self.conversationsTableView)
-    }
-
-    let searchController: UISearchController = {
-        let searchController = UISearchController(searchResultsController: nil)
-        searchController.obscuresBackgroundDuringPresentation = false
-        searchController.definesPresentationContext = true
-        searchController.hidesNavigationBarDuringPresentation = true
-        return searchController
-    }()
-
-    func setupSearchBar() {
-        searchController.delegate = self
-        let navBar = SmartListNavigationBar()
-        self.navigationController?.setValue(navBar, forKey: "navigationBar")
-
-        navigationItem.searchController = searchController
-        if #available(iOS 16.0, *) {
-            navigationItem.preferredSearchBarPlacement = .stacked
-        }
-
-        navigationItem.hidesSearchBarWhenScrolling = false
-        searchView.searchBar = searchController.searchBar
-        self.searchView.editSearch
-            .subscribe(onNext: {[weak self] (editing) in
-                self?.viewModel.searching.onNext(editing)
-            })
-            .disposed(by: disposeBag)
-    }
-
-    func willPresentSearchController(_ searchController: UISearchController) {
-        self.searchBarActive()
-    }
-
-    func updateSearchBarIfActive() {
-        if searchController.isActive {
-            searchBarActive()
-        }
-    }
-
-    func updateNetworkUI() {
-        let isHidden = self.viewModel.networkConnectionState() == .none ? false : true
-        self.networkAlertView.isHidden = isHidden
-        self.view.layoutIfNeeded()
-    }
-
-    func searchBarNotActive() {
-        guard let customNavBar = self.navigationController?.navigationBar as? SmartListNavigationBar else { return }
-        self.navigationItem.title = selectedSegmentIndex.value == 0 ?
-            L10n.Smartlist.conversations : L10n.Smartlist.invitations
-        self.widgetsTopConstraint.constant = 0
-        updateNetworkUI()
-        customNavBar.customHeight = 44
-        customNavBar.searchActive = false
-        customNavBar.removeTopView()
-    }
-
-    func searchBarActive() {
-        guard let customNavBar = navigationController?.navigationBar as? SmartListNavigationBar else { return }
-
-        setupCommonUI(customNavBar: customNavBar)
-
-        if viewModel.isSipAccount() {
-            setupUIForSipAccount(customNavBar: customNavBar)
-        } else {
-            setupUIForNonSipAccount(customNavBar: customNavBar)
-        }
-    }
-
-    private func setupCommonUI(customNavBar: SmartListNavigationBar) {
-        navigationItem.title = ""
-        widgetsTopConstraint.constant = 42
-        customNavBar.customHeight = 70
-        customNavBar.searchActive = true
-    }
-
-    private func setupUIForSipAccount(customNavBar: SmartListNavigationBar) {
-        let bookButton = createSearchBarButtonWithImage(named: "book.circle", weight: .regular, width: 27)
-        bookButton.setImage(UIImage(asset: Asset.phoneBook), for: .normal)
-        bookButton.rx.tap
-            .subscribe(onNext: { [weak self] in
-                self?.presentContactPicker()
-            })
-            .disposed(by: customNavBar.disposeBag)
-
-        let dialpadCodeButton = createSearchBarButtonWithImage(named: "square.grid.3x3.topleft.filled", weight: .regular, width: 25)
-        dialpadCodeButton.rx.tap
-            .subscribe(onNext: { [weak self] in
-                self?.viewModel.showDialpad()
-            })
-            .disposed(by: customNavBar.disposeBag)
-
-        customNavBar.addTopView(with: [bookButton, dialpadCodeButton])
-    }
-
-    private func setupUIForNonSipAccount(customNavBar: SmartListNavigationBar) {
-        let qrCodeButton = createSearchBarButtonWithImage(named: "qrcode", weight: .medium, width: 25)
-        qrCodeButton.rx.tap
-            .subscribe(onNext: { [weak self] in
-                self?.viewModel.showQRCode()
-            })
-            .disposed(by: customNavBar.disposeBag)
-
-        let swarmButton = createSearchBarButtonWithImage(named: "person.2", weight: .medium, width: 32)
-        swarmButton.rx.tap
-            .subscribe(onNext: { [weak self] in
-                self?.viewModel.createGroup()
-            })
-            .disposed(by: customNavBar.disposeBag)
-
-        customNavBar.addTopView(with: [qrCodeButton, swarmButton])
-    }
-
-    private func presentContactPicker() {
-        contactPicker.delegate = self
-        present(contactPicker, animated: true, completion: nil)
-    }
-
-    private func createSearchBarButtonWithImage(named imageName: String, weight: UIImage.SymbolWeight, width: CGFloat) -> UIButton {
-        let button = UIButton()
-        let configuration = UIImage.SymbolConfiguration(pointSize: 40, weight: weight, scale: .large)
-        button.setImage(UIImage(systemName: imageName, withConfiguration: configuration), for: .normal)
-        button.tintColor = .jamiButtonDark
-        button.translatesAutoresizingMaskIntoConstraints = false
-        button.widthAnchor.constraint(equalToConstant: width).isActive = true
-        button.heightAnchor.constraint(equalToConstant: 23).isActive = true
-        return button
-    }
-
-    func willDismissSearchController(_ searchController: UISearchController) {
-        searchBarNotActive()
-    }
-
-    func startAccountCreation() {
-        accountPickerTextView.resignFirstResponder()
-        self.viewModel.createAccount()
-    }
-
-    func openAccountsList() {
-        if searchView.searchBar.isFirstResponder {
-            return
-        }
-        if accountPickerTextView.isFirstResponder {
-            accountPickerTextView.resignFirstResponder()
-            return
-        }
-        accountPickerTextView.becomeFirstResponder()
-        self.view.addGestureRecognizer(accountsDismissTapRecognizer)
-    }
-
-    private func showRemoveConversationConfirmation(atIndex: IndexPath) {
-        let alert = UIAlertController(title: L10n.Alerts.confirmDeleteConversationTitle, message: L10n.Alerts.confirmDeleteConversation, preferredStyle: .alert)
-        let deleteAction = UIAlertAction(title: L10n.Actions.deleteAction, style: .destructive) { (_: UIAlertAction!) -> Void in
-            if let convToDelete: ConversationViewModel = try? self.conversationsTableView.rx.model(at: atIndex) {
-                self.viewModel.delete(conversationViewModel: convToDelete)
-            }
-        }
-        let cancelAction = UIAlertAction(title: L10n.Global.cancel, style: .default) { (_: UIAlertAction!) -> Void in }
-        alert.addAction(deleteAction)
-        alert.addAction(cancelAction)
-        self.present(alert, animated: true, completion: nil)
-    }
-
-    private func showBlockContactConfirmation(atIndex: IndexPath) {
-        let alert = UIAlertController(title: L10n.Global.blockContact, message: L10n.Alerts.confirmBlockContact, preferredStyle: .alert)
-        let blockAction = UIAlertAction(title: L10n.Global.block, style: .destructive) { (_: UIAlertAction!) -> Void in
-            if let conversation: ConversationViewModel = try? self.conversationsTableView.rx.model(at: atIndex) {
-                self.viewModel.blockConversationsContact(conversationViewModel: conversation)
-            }
-        }
-        let cancelAction = UIAlertAction(title: L10n.Global.cancel, style: .default) { (_: UIAlertAction!) -> Void in }
-        alert.addAction(blockAction)
-        alert.addAction(cancelAction)
-        self.present(alert, animated: true, completion: nil)
-    }
-}
-
-extension SmartlistViewController: UITableViewDelegate {
-
-    func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? {
-        let block = UITableViewRowAction(style: .normal, title: "Block") { _, index in
-            self.showBlockContactConfirmation(atIndex: index)
-        }
-        block.backgroundColor = .red
-
-        let delete = UITableViewRowAction(style: .normal, title: "Delete") { _, index in
-            self.showRemoveConversationConfirmation(atIndex: index)
-        }
-        delete.backgroundColor = .orange
-
-        return [delete, block]
-    }
-
-    private func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
-        return true
-    }
-
-    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        if self.navigationController?.topViewController == self {
-            if let convToShow: ConversationViewModel = try? tableView.rx.model(at: indexPath) {
-                self.viewModel.showConversation(withConversationViewModel: convToShow)
-            }
-        }
-    }
-}
-
-extension SmartlistViewController: CNContactPickerDelegate {
-
-    func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
-        let phoneNumberCount = contact.phoneNumbers.count
-        guard phoneNumberCount > 0 else {
-            dismiss(animated: true)
-            let alert = UIAlertController(title: L10n.Smartlist.noNumber,
-                                          message: nil,
-                                          preferredStyle: .alert)
-            let cancelAction = UIAlertAction(title: L10n.Global.ok,
-                                             style: .default) { (_: UIAlertAction!) -> Void in }
-            alert.addAction(cancelAction)
-            self.present(alert, animated: true, completion: nil)
-            return
-        }
-
-        if phoneNumberCount == 1 {
-            setNumberFromContact(contactNumber: contact.phoneNumbers[0].value.stringValue)
-        } else {
-            let alert = UIAlertController(title: L10n.Smartlist.selectOneNumber, message: nil, preferredStyle: .alert)
-            for contact in contact.phoneNumbers {
-                let contactAction = UIAlertAction(title: contact.value.stringValue,
-                                                  style: .default) { [weak self](_: UIAlertAction!) -> Void in
-                    self?.setNumberFromContact(contactNumber: contact.value.stringValue)
-                }
-                alert.addAction(contactAction)
-            }
-            let cancelAction = UIAlertAction(title: L10n.Global.cancel,
-                                             style: .default) { (_: UIAlertAction!) -> Void in }
-            alert.addAction(cancelAction)
-            dismiss(animated: true)
-            self.present(alert, animated: true, completion: nil)
-        }
-    }
-
-    func setNumberFromContact(contactNumber: String) {
-        self.viewModel.showSipConversation(withNumber: contactNumber.trimmedSipNumber())
-    }
-
-    func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
-
-    }
-}
-
-// MARK: - menu
-extension SmartlistViewController {
-    private func createMenu() -> UIMenu {
-        return UIMenu(title: "", children: [createSwarmAction(), inviteFriendsAction(), accountsAction(), openAccountAction(), openSettingsAction(), donateAction(), aboutJamiAction()])
-    }
-
-    private func createTintedImage(systemName: String, configuration: UIImage.SymbolConfiguration, tintColor: UIColor) -> UIImage? {
-        let image = UIImage(systemName: systemName, withConfiguration: configuration)
-        return image?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
-    }
-
-    // MARK: - Action creation functions
-
-    private var configuration: UIImage.SymbolConfiguration {
-        return UIImage.SymbolConfiguration(scale: .medium)
-    }
-
-    private func createSwarmAction() -> UIAction {
-        let image = createTintedImage(systemName: "person.2", configuration: configuration, tintColor: .jamiButtonDark)
-        return UIAction(title: L10n.Swarm.newSwarm, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.viewModel.createGroup()
-        }
-    }
-
-    private func inviteFriendsAction() -> UIAction {
-        let image = createTintedImage(systemName: "envelope.open", configuration: configuration, tintColor: .jamiButtonDark)
-        return UIAction(title: L10n.Smartlist.inviteFriends, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.shareAccountInfo()
-        }
-    }
-
-    private func donateAction() -> UIAction {
-        let image = createTintedImage(systemName: "heart", configuration: configuration, tintColor: .jamiDonation)
-        return UIAction(title: L10n.Global.donate, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { _ in
-            SharedActionsPresenter.openDonationLink()
-        }
-    }
-
-    private func accountsAction() -> UIAction {
-        let image = createTintedImage(systemName: "list.bullet", configuration: configuration, tintColor: .jamiButtonDark)
-        return UIAction(title: L10n.Smartlist.accounts, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.openAccountsList()
-        }
-    }
-
-    private func openAccountAction() -> UIAction {
-        let image = createTintedImage(systemName: "person.circle", configuration: configuration, tintColor: .jamiButtonDark)
-        return UIAction(title: L10n.Global.accountSettings, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.viewModel.showAccountSettings()
-        }
-    }
-
-    private func openSettingsAction() -> UIAction {
-        let image = createTintedImage(systemName: "gearshape", configuration: configuration, tintColor: .jamiButtonDark)
-        return UIAction(title: L10n.Global.advancedSettings, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.viewModel.showGeneralSettings()
-        }
-    }
-
-    private func aboutJamiAction() -> UIAction {
-        let image = UIImage(asset: Asset.jamiIcon)?.resizeImageWith(newSize: CGSize(width: 22, height: 22), opaque: false)
-        return UIAction(title: L10n.Smartlist.aboutJami, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
-            self?.viewModel.openAboutJami()
-        }
+    func addSwiftUI() {
+        let contentView = UIHostingController(rootView: SmartListContainer(model: viewModel.conversationsModel))
+        addChild(contentView)
+        view.addSubview(contentView.view)
+        contentView.view.frame = self.view.bounds
+        contentView.view.translatesAutoresizingMaskIntoConstraints = false
+        contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
+        contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+        contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
+        contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
+        contentView.didMove(toParent: self)
     }
 }
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
index 3796d9c..602dfc0 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2023 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2024 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -21,17 +21,10 @@
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
  */
 
+import UIKit
 import RxSwift
-import RxCocoa
-import SwiftyBeaver
-import RxRelay
 
-let smartListAccountSize: CGFloat = 28
-let smartListAccountMargin: CGFloat = 4
-
-class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource {
-
-    private let log = SwiftyBeaver.self
+class SmartlistViewModel: Stateable, ViewModel {
 
     // MARK: - Rx Stateable
     private let stateSubject = PublishSubject<State>()
@@ -39,407 +32,15 @@
         return self.stateSubject.asObservable()
     }()
 
-    private let disposeBag = DisposeBag()
-    private var tempBag = DisposeBag()
-
-    // Services
-    private let conversationsService: ConversationsService
-    private let nameService: NameService
-    private let accountsService: AccountsService
-    private let contactsService: ContactsService
-    private let networkService: NetworkService
-    private let profileService: ProfilesService
-    private let callService: CallsService
-    private let requestsService: RequestsService
-
-    var currentAccount: AccountModel? { self.accountsService.currentAccount }
-
-    var searching = PublishSubject<Bool>()
-
-    private var contactFoundConversation = BehaviorRelay<ConversationViewModel?>(value: nil)
-
-    lazy var hideNoConversationsMessage: Observable<Bool> = {
-        return Observable<Bool>
-            .combineLatest(self.conversations,
-                           self.searching.asObservable().startWith(false),
-                           resultSelector: {(conversations, searching) -> Bool in
-                            if searching { return true }
-                            if let convf = conversations.first {
-                                return !convf.items.isEmpty
-                            }
-                            return false
-                           })
-            .observe(on: MainScheduler.instance)
-    }()
-
-    var connectionState = PublishSubject<ConnectionType>()
-    lazy var accounts: Observable<[AccountItem]> = {
-        return self.accountsService
-            .accountsObservable.asObservable()
-            .map({ [weak self] accountsModels in
-                var items = [AccountItem]()
-                guard let self = self else { return items }
-                for account in accountsModels {
-                    items.append(AccountItem(account: account,
-                                             profileObservable: self.profileService.getAccountProfile(accountId: account.id)))
-                }
-                return items
-            })
-    }()
-
-    var donationBannerVisible = BehaviorRelay(value: false)
-
-    /// For FilterConversationDataSource protocol
-    var conversationViewModels = [ConversationViewModel]()
-
-    func networkConnectionState() -> ConnectionType {
-        return self.networkService.connectionState.value
-    }
-
     let injectionBag: InjectionBag
-    // Values need to be updated when selected account changed
-    var profileImageForCurrentAccount = PublishSubject<Profile>()
-
-    lazy var profileImage: Observable<UIImage> = { [weak self] in
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: {
-            if let self = self, let account = self.accountsService.currentAccount {
-                self.profileService.getAccountProfile(accountId: account.id)
-                    .subscribe(onNext: { profile in
-                        self.profileImageForCurrentAccount.onNext(profile)
-                    })
-                    .disposed(by: self.tempBag)
-            }
-        })
-        return profileImageForCurrentAccount.share()
-            .map({ profile in
-                let size = smartListAccountSize - (smartListAccountMargin * 3)
-                if let photo = profile.photo,
-                   let data = NSData(base64Encoded: photo,
-                                     options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data?,
-                   let image = UIImage(data: data) {
-                    return image
-                }
-                return UIImage.defaultJamiAvatarFor(profileName: profile.alias, account: self?.accountsService.currentAccount, size: size)
-            })
-            .startWith(UIImage(asset: Asset.icContactPicture)!)
-    }()
-    lazy var accountName: Observable<String> = { [weak self] in
-        return profileImageForCurrentAccount.share()
-            .map({ profile in
-                if let alias = profile.alias {
-                    if !alias.isEmpty { return alias }
-                }
-                guard let account = self?.accountsService.currentAccount else {
-                    return ""
-                }
-                return account.registeredName.isEmpty ? account.jamiId : account.registeredName
-            })
-            .startWith("")
-    }()
-
-    lazy var conversations: Observable<[ConversationSection]> = { [weak self] in
-        guard let self = self else { return Observable.empty() }
-        return self.conversationsService
-            .conversations
-            .share()
-            .startWith(self.conversationsService.conversations.value)
-            .map({ (conversations) in
-                if conversations.isEmpty {
-                    self.conversationViewModels = [ConversationViewModel]()
-                }
-                return conversations
-                    .compactMap({ conversationModel in
-                        var conversationViewModel: ConversationViewModel?
-                        if let foundConversationViewModel = self.conversationViewModels.filter({ conversationViewModel in
-                            return conversationViewModel.conversation == conversationModel
-                        }).first {
-                            conversationViewModel = foundConversationViewModel
-                            conversationViewModel?.conversation = conversationModel
-                        } else if let contactFound = self.contactFoundConversation.value, contactFound.conversation == conversationModel {
-                            conversationViewModel = contactFound
-                            conversationViewModel?.conversation = conversationModel
-                            conversationViewModel?.conversationCreated.accept(true)
-                            self.conversationViewModels.append(contactFound)
-                        } else {
-                            conversationViewModel = ConversationViewModel(with: self.injectionBag)
-                            conversationViewModel?.conversation = conversationModel
-                            if let conversation = conversationViewModel {
-                                self.conversationViewModels
-                                    .append(conversation)
-                            }
-                        }
-                        return conversationViewModel
-                    })
-            })
-            .map({ conversationsViewModels in
-                return [ConversationSection(header: "", items: conversationsViewModels)]
-            })
-    }()
-
-    lazy var unreadMessages: Observable<Int> = {[weak self] in
-        guard let self = self else {
-            return Observable.just(0)
-        }
-        return self.conversationsService.conversations
-            .share()
-            .flatMap { conversations -> Observable<[Int]> in
-                return Observable.combineLatest(conversations.map({ $0.numberOfUnreadMessages }))
-            }
-            .map { unreadMessages in
-                return unreadMessages.reduce(0, +)
-            }
-    }()
-
-    var accountInfoToShare: [Any]? {
-        return self.accountsService.accountInfoToShare
-    }
-
-    lazy var unhandeledRequests: Observable<Int> = {[weak self] in
-        guard let self = self else {
-            return Observable.just(0)
-        }
-        let requestObservable = self.requestsService.requests.asObservable()
-        let conversationObservable = self.conversationsService
-            .conversations
-            .share()
-            .startWith(self.conversationsService.conversations.value)
-
-        return  Observable.combineLatest(requestObservable,
-                                         conversationObservable) { [weak self] (requests, conversations) -> Int in
-            guard let self = self,
-                  let account = self.accountsService.currentAccount else {
-                return 0
-            }
-            // filter out existing conversations
-            let conversationIds = conversations.map { $0.id }
-            let filteredRequests = requests.filter {
-                $0.accountId == account.id && !conversationIds.contains($0.conversationId)
-            }
-            return filteredRequests.count
-        }
-    }()
-
-    typealias BageValues = (messages: Int, requests: Int)
-
-    lazy var updateSegmentControl: ReplaySubject<BageValues> = {
-        let subject = ReplaySubject<BageValues>.create(bufferSize: 1)
-
-        Observable.combineLatest(self.unreadMessages, self.unhandeledRequests) { (messages, requests) -> BageValues in
-            return (messages, requests)
-        }
-        .observe(on: MainScheduler.instance)
-        .subscribe(onNext: { values in
-            subject.onNext(values)
-        }, onError: { error in
-            subject.onError(error)
-        }, onCompleted: {
-            subject.onCompleted()
-        })
-        .disposed(by: self.disposeBag)
-
-        return subject
-    }()
-
-    func reloadDataFor(accountId: String) {
-        tempBag = DisposeBag()
-        self.profileService.getAccountProfile(accountId: accountId)
-            .subscribe(onNext: { [weak self] profile in
-                self?.profileImageForCurrentAccount.onNext(profile)
-            })
-            .disposed(by: self.tempBag)
-    }
-
-    lazy var currentAccountChanged: Observable<AccountModel?> = {
-        return self.accountsService.currentAccountChanged.asObservable()
-    }()
+    let conversationsModel: ConversationsViewModel
 
     required init(with injectionBag: InjectionBag) {
-        self.conversationsService = injectionBag.conversationsService
-        self.nameService = injectionBag.nameService
-        self.accountsService = injectionBag.accountService
-        self.contactsService = injectionBag.contactsService
-        self.networkService = injectionBag.networkService
-        self.profileService = injectionBag.profileService
-        self.callService = injectionBag.callService
-        self.requestsService = injectionBag.requestsService
         self.injectionBag = injectionBag
-        self.updateDonationBunnerVisiblity()
-
-        self.callService.newCall
-            .asObservable()
-            .observe(on: MainScheduler.instance)
-            .subscribe(onNext: { [weak self] _ in
-                self?.closeAllPlayers()
-            })
-            .disposed(by: self.disposeBag)
-
-        self.accountsService.currentAccountChanged
-            .subscribe(onNext: { [weak self] account in
-                if let currentAccount = account {
-                    self?.reloadDataFor(accountId: currentAccount.id)
-                }
-            })
-            .disposed(by: self.disposeBag)
-
-        // Observe connectivity changes
-        self.networkService.connectionStateObservable
-            .subscribe(onNext: { [weak self] value in
-                self?.connectionState.onNext(value)
-            })
-            .disposed(by: self.disposeBag)
-
-        // Observe conversation removed
-        self.conversationsService.sharedResponseStream
-            .filter({ event in
-                event.eventType == .conversationRemoved && event.getEventInput(.accountId) == self.currentAccount?.id
-            })
-            .subscribe(onNext: { [weak self] event in
-                guard let conversationId: String = event.getEventInput(.conversationId),
-                      let accountId: String = event.getEventInput(.accountId) else { return }
-                guard let index = self?.conversationViewModels.firstIndex(where: { conversationModel in
-                    conversationModel.conversation.id == conversationId && conversationModel.conversation.accountId == accountId
-                }) else { return }
-                self?.conversationViewModels.remove(at: index)
-            })
-            .disposed(by: self.disposeBag)
-    }
-
-    func getDonationBunnerVisiblity() -> Bool {
-        return PreferenceManager.isDateWithinCampaignPeriod() && PreferenceManager.isCampaignEnabled()
-    }
-
-    func updateDonationBunnerVisiblity() {
-        self.donationBannerVisible.accept(getDonationBunnerVisiblity())
-    }
-
-    func temporaryDisableDonationCampaign() {
-        PreferenceManager.temporarilyDisableDonationCampaign()
-        self.donationBannerVisible.accept(getDonationBunnerVisiblity())
-    }
-
-    func delete(conversationViewModel: ConversationViewModel) {
-        conversationViewModel.closeAllPlayers()
-        let accountId = conversationViewModel.conversation.accountId
-        let conversationId = conversationViewModel.conversation.id
-        if conversationViewModel.conversation.isCoredialog(),
-           let participantId = conversationViewModel.conversation.getParticipants().first?.jamiId {
-            self.contactsService
-                .removeContact(withId: participantId,
-                               ban: false,
-                               withAccountId: accountId)
-                .asObservable()
-                .subscribe(onCompleted: { [weak self, weak conversationViewModel] in
-                    guard let conversationViewModel = conversationViewModel else { return }
-                    self?.conversationsService
-                        .removeConversationFromDB(conversation: conversationViewModel.conversation,
-                                                  keepConversation: false)
-                })
-                .disposed(by: self.disposeBag)
-        } else {
-            self.conversationsService.removeConversation(conversationId: conversationId, accountId: accountId)
-        }
-    }
-
-    func blockConversationsContact(conversationViewModel: ConversationViewModel) {
-        conversationViewModel.closeAllPlayers()
-        let accountId = conversationViewModel.conversation.accountId
-        let conversationId = conversationViewModel.conversation.id
-        if conversationViewModel.conversation.isCoredialog(),
-           let participantId = conversationViewModel.conversation.getParticipants().first?.jamiId {
-            self.contactsService
-                .removeContact(withId: participantId,
-                               ban: true,
-                               withAccountId: accountId)
-                .asObservable()
-                .subscribe(onCompleted: { [weak self, weak conversationViewModel] in
-                    guard let conversationViewModel = conversationViewModel else { return }
-                    self?.conversationsService
-                        .removeConversationFromDB(conversation: conversationViewModel.conversation,
-                                                  keepConversation: false)
-                })
-                .disposed(by: self.disposeBag)
-        } else {
-            self.conversationsService.removeConversation(conversationId: conversationId, accountId: accountId)
-        }
-    }
-
-    func showAccountSettings() {
-        self.stateSubject.onNext(ConversationState.showAccountSettings)
+        self.conversationsModel = ConversationsViewModel(injectionBag: injectionBag, stateSubject: self.stateSubject)
     }
 
     func closeAllPlayers() {
-        self.conversationViewModels.forEach { conversationModel in
-            conversationModel.closeAllPlayers()
-        }
-    }
-
-    func isSipAccount() -> Bool {
-        guard let account = self.currentAccount else { return false }
-        return account.type == .sip
-    }
-
-    func showSipConversation(withNumber number: String) {
-        guard let account = self.accountsService
-                .currentAccount else {
-            return
-        }
-        let uri = JamiURI.init(schema: URIType.sip,
-                               infoHash: number,
-                               account: account)
-        let conversation = ConversationModel(withParticipantUri: uri,
-                                             accountId: account.id,
-                                             hash: number)
-        conversation.type = .sip
-        let newConversation = ConversationViewModel(with: self.injectionBag)
-        newConversation.conversation = conversation
-        self.stateSubject
-            .onNext(ConversationState
-                        .conversationDetail(conversationViewModel:
-                                                newConversation))
-    }
-
-    func showQRCode() {
-        self.stateSubject.onNext(ConversationState.qrCode)
-    }
-    func createGroup() {
-        self.stateSubject.onNext(ConversationState.createSwarm)
-    }
-
-    func createAccount() {
-        self.stateSubject.onNext(ConversationState.createNewAccount)
-    }
-
-    func changeCurrentAccount(accountId: String) {
-        if let account = self.accountsService.getAccount(fromAccountId: accountId) {
-            if accountsService.needAccountMigration(accountId: accountId) {
-                self.stateSubject.onNext(ConversationState.needAccountMigration(accountId: accountId))
-                return
-            }
-            self.accountsService.updateCurrentAccount(account: account)
-            UserDefaults.standard.set(accountId, forKey: self.accountsService.selectedAccountID)
-        }
-    }
-
-    func showDialpad() {
-        self.stateSubject.onNext(ConversationState.showDialpad(inCall: false))
-    }
-
-    func showGeneralSettings() {
-        self.stateSubject.onNext(ConversationState.showGeneralSettings)
-    }
-
-    func openAboutJami() {
-        self.stateSubject.onNext(ConversationState.openAboutJami)
-    }
-}
-
-extension SmartlistViewModel: FilterConversationDelegate {
-    func temporaryConversationCreated(conversation: ConversationViewModel?) {
-        self.contactFoundConversation.accept(conversation)
-    }
-
-    func showConversation(withConversationViewModel conversationViewModel: ConversationViewModel) {
-        self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel:
-                                                                        conversationViewModel))
+        self.conversationsModel.closeAllPlayers()
     }
 }
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/AccountsViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/AccountsViewModel.swift
new file mode 100644
index 0000000..b46b44b
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/AccountsViewModel.swift
@@ -0,0 +1,182 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import Foundation
+import SwiftUI
+import RxSwift
+
+protocol AccountProfileObserver: AnyObject {
+    var avatar: UIImage { get set }
+    var profileName: String { get set }
+    var registeredName: String { get set }
+    var bestName: String { get set }
+    var disposeBag: DisposeBag { get }
+    var profileService: ProfilesService { get }
+}
+
+extension AccountProfileObserver {
+    func updateProfileDetails(account: AccountModel) {
+        profileService.getAccountProfile(accountId: account.id)
+            .subscribe(onNext: { profile in
+                let avatar = profile.photo?.createImage() ?? UIImage.defaultJamiAvatarFor(profileName: profile.alias, account: account, size: 17)
+                DispatchQueue.main.async { [weak self] in
+                    guard let self = self else { return }
+                    self.avatar = avatar
+                    self.profileName = profile.alias ?? ""
+                    self.updateBestName()
+                }
+            })
+            .disposed(by: disposeBag)
+    }
+
+    func resolveAccountName(from account: AccountModel) -> String {
+        if !account.registeredName.isEmpty {
+            return account.registeredName
+        }
+        if let userNameData = UserDefaults.standard.dictionary(forKey: registeredNamesKey),
+           let accountName = userNameData[account.id] as? String,
+           !accountName.isEmpty {
+            return accountName
+        }
+        return account.jamiId
+    }
+
+    func updateBestName() {
+        self.bestName = profileName.isEmpty ? registeredName : profileName
+    }
+}
+
+struct AccountRowSizes {
+    let imageSize: CGFloat = 28
+    let spacing: CGFloat = 15
+}
+
+class AccountRow: ObservableObject, Hashable, Identifiable, AccountProfileObserver {
+    let id: String
+
+    @Published var avatar = UIImage()
+    @Published var profileName: String = ""
+    @Published var registeredName: String = ""
+    @Published var bestName: String = ""
+    @Published var needMigrate: String?
+
+    var dimensions = AccountRowSizes()
+
+    var disposeBag = DisposeBag()
+    var profileService: ProfilesService
+    var account: AccountModel
+
+    init(account: AccountModel, profileService: ProfilesService) {
+        self.id = account.id
+        self.profileService = profileService
+        self.account = account
+        if account.status == .errorNeedMigration {
+            needMigrate = L10n.Account.needMigration
+        }
+
+        self.registeredName = resolveAccountName(from: account)
+        updateProfileDetails(account: account)
+    }
+
+    func hash(into hasher: inout Hasher) {
+        return hasher.combine(id)
+    }
+
+    static func == (lhs: AccountRow, rhs: AccountRow) -> Bool {
+        return lhs.id == rhs.id
+    }
+}
+
+class AccountsViewModel: ObservableObject, AccountProfileObserver {
+    @Published var avatar = UIImage()
+    @Published var profileName: String = ""
+    @Published var registeredName: String = ""
+    @Published var bestName: String = ""
+    @Published var selectedAccount: String?
+    @Published var accountsRows: [AccountRow] = []
+
+    let headerTitle = L10n.Smartlist.accounts
+
+    var dimensions = AccountRowSizes()
+
+    let accountService: AccountsService
+    let profileService: ProfilesService
+    let nameService: NameService
+    var disposeBag = DisposeBag()
+    let stateSubject: PublishSubject<State>
+
+    init(accountService: AccountsService, profileService: ProfilesService, nameService: NameService, stateSubject: PublishSubject<State>) {
+        self.accountService = accountService
+        self.profileService = profileService
+        self.nameService = nameService
+        self.stateSubject = stateSubject
+        self.subscribeToCurrentAccountUpdates()
+        self.subscribeToRegisteredName()
+    }
+
+    func subscribeToCurrentAccountUpdates() {
+        accountService.currentAccountChanged
+            .startWith(accountService.currentAccount)
+            .compactMap { $0 }
+            .subscribe(onNext: { [weak self] account in
+                guard let self = self else { return }
+                self.selectedAccount = account.id
+                self.registeredName = self.resolveAccountName(from: account)
+                self.updateProfileDetails(account: account)
+            })
+            .disposed(by: disposeBag)
+    }
+
+    func subscribeToRegisteredName() {
+        self.nameService.sharedRegistrationStatus
+            .filter({ (serviceEvent) -> Bool in
+                guard let account = self.accountService.currentAccount else { return false }
+                guard serviceEvent.getEventInput(ServiceEventInput.accountId) == account.id,
+                      serviceEvent.eventType == .nameRegistrationEnded,
+                      let status: NameRegistrationState = serviceEvent.getEventInput(ServiceEventInput.state),
+                      status == .success else {
+                    return false
+                }
+                return true
+            })
+            .subscribe(onNext: { [weak self] _ in
+                guard let self = self else { return }
+                guard let account = self.accountService.currentAccount else { return }
+                self.updateProfileDetails(account: account)
+            })
+            .disposed(by: disposeBag)
+    }
+
+    func getAccountsRows() {
+        accountsRows = self.accountService.accounts.map { accountModel in
+            return AccountRow(account: accountModel, profileService: self.profileService)
+        }
+    }
+
+    func changeCurrentAccount(accountId: String) {
+        guard let account = self.accountService.getAccount(fromAccountId: accountId) else { return }
+        if accountService.needAccountMigration(accountId: accountId) {
+            self.stateSubject.onNext(ConversationState.needAccountMigration(accountId: accountId))
+            return
+        }
+        self.accountService.updateCurrentAccount(account: account)
+        UserDefaults.standard.set(accountId, forKey: self.accountService.selectedAccountID)
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/ConversationsViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/ConversationsViewModel.swift
new file mode 100644
index 0000000..b3637c4
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/ConversationsViewModel.swift
@@ -0,0 +1,486 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import Foundation
+import SwiftUI
+import RxSwift
+import RxRelay
+
+// swiftlint:disable type_body_length
+class ConversationsViewModel: ObservableObject, FilterConversationDataSource {
+    // filtered conversations to display
+    @Published var conversations = [ConversationViewModel]()
+    // temporary conversation for jami or sip
+    @Published var temporaryConversation: ConversationViewModel? {
+        didSet { updateSearchStatusIfNeeded() }
+    }
+    // jams search  result
+    @Published var jamsSearchResult = [ConversationViewModel]() {
+        didSet { updateSearchStatusIfNeeded() }
+    }
+    // all conversations
+    var conversationViewModels = [ConversationViewModel]() {
+        didSet {
+            self.updateConversations()
+        }
+    }
+
+    @Published var publicDirectoryTitle = L10n.Smartlist.results
+
+    @Published var selectedSegment = 0
+    @Published var unreadMessages = 0
+    @Published var searchingLabel = ""
+    @Published var connectionState: ConnectionType = .none
+    var disposeBag = DisposeBag()
+    let conversationsService: ConversationsService
+    let requestsService: RequestsService
+    let accountsService: AccountsService
+    let contactsService: ContactsService
+    let stateSubject: PublishSubject<State>
+    let injectionBag: InjectionBag
+    var searchModel: JamiSearchViewModel?
+    var requestsModel: RequestsViewModel
+    @Published var searchQuery: String = ""
+    @Published var conversationCreated: String = ""
+    @Published var searchStatus: SearchStatus = .notSearching
+    let jamiImage = UIImage(asset: Asset.jamiIcon)!.resizeImageWith(newSize: CGSize(width: 20, height: 20), opaque: false)!
+
+    var accountsModel: AccountsViewModel
+
+    init(injectionBag: InjectionBag, stateSubject: PublishSubject<State>) {
+        self.conversationsService = injectionBag.conversationsService
+        self.requestsService = injectionBag.requestsService
+        self.accountsService = injectionBag.accountService
+        self.contactsService = injectionBag.contactsService
+        self.accountsModel =
+            AccountsViewModel(accountService: injectionBag.accountService,
+                              profileService: injectionBag.profileService,
+                              nameService: injectionBag.nameService,
+                              stateSubject: stateSubject)
+        self.injectionBag = injectionBag
+        self.stateSubject = stateSubject
+        self.requestsModel = RequestsViewModel(injectionBag: injectionBag)
+        self.searchModel = JamiSearchViewModel(with: injectionBag, source: self, searchOnlyExistingConversations: false)
+        self.subscribeConversations()
+        self.subscribeSearch()
+        injectionBag.networkService.connectionState
+            .startWith(injectionBag.networkService.connectionState.value)
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] state in
+                self?.connectionState = state
+            })
+            .disposed(by: self.disposeBag)
+        self.accountsService.currentAccountChanged
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] account in
+                if let account = account {
+                    if account.isJams {
+                        self?.publicDirectoryTitle = L10n.Smartlist.jamsResults
+                    } else {
+                        self?.publicDirectoryTitle = L10n.Smartlist.results
+                    }
+                }
+            })
+            .disposed(by: self.disposeBag)
+        if let account = self.accountsService.currentAccount, account.isJams {
+            publicDirectoryTitle = L10n.Smartlist.jamsResults
+        }
+    }
+
+    func conversationFromTemporaryCreated(conversation: ConversationModel) {
+        // If conversation created from temporary navigate back to smart list
+        if self.presentedConversation.isTemporaryPresented() {
+            navigationTarget = .smartList
+            self.presentedConversation.resetPresentedConversation()
+        }
+        // cleanup search
+        self.performSearch(query: "")
+        // disable search bar
+        conversationCreated = conversation.id
+    }
+
+    private func subscribeConversations() {
+        let conversationObservable = self.conversationsService.conversations
+            .share()
+            .startWith(self.conversationsService.conversations.value)
+        let conersationViewModels =
+            conversationObservable.map { [weak self] conversations -> [ConversationViewModel] in
+                guard let self = self else { return [] }
+
+                // Reset conversationViewModels if conversations are empty
+                if conversations.isEmpty {
+                    self.conversationViewModels.removeAll()
+                    return []
+                }
+
+                // Map conversations to view models, updating existing ones or creating new
+                return conversations.compactMap { conversationModel in
+                    // Check for existing conversation view model
+                    if let existing = self.conversationViewModels.first(where: { $0.conversation == conversationModel }) {
+                        return existing
+                    }
+                    // Check for temporary conversation
+                    else if let tempConversation = self.temporaryConversation, tempConversation.conversation == conversationModel {
+                        tempConversation.conversation = conversationModel
+                        self.conversationViewModels.append(tempConversation)
+                        tempConversation.isTemporary.accept(false)
+                        tempConversation.conversationCreated.accept(true)
+                        self.conversationFromTemporaryCreated(conversation: conversationModel)
+                        return tempConversation
+                    } else if let jamsConversation = self.jamsSearchResult.first(where: { jams in
+                        jams.conversation == conversationModel
+                    }) {
+                        jamsConversation.conversation = conversationModel
+                        self.conversationViewModels.append(jamsConversation)
+                        jamsConversation.isTemporary.accept(false)
+                        jamsConversation.conversationCreated.accept(true)
+                        self.conversationFromTemporaryCreated(conversation: conversationModel)
+                        return jamsConversation
+                    }
+                    // Create new conversation view model
+                    else {
+                        let newViewModel = ConversationViewModel(with: self.injectionBag)
+                        newViewModel.conversation = conversationModel
+                        self.conversationViewModels.append(newViewModel)
+                        return newViewModel
+                    }
+                }
+            }
+
+        conersationViewModels
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] updatedViewModels in
+                self?.conversationViewModels = updatedViewModels
+            })
+            .disposed(by: self.disposeBag)
+
+        // Observe conversation removed
+        self.conversationsService.sharedResponseStream
+            .filter({ event in
+                event.eventType == .conversationRemoved && event.getEventInput(.accountId) == self.accountsService.currentAccount?.id
+            })
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] event in
+                guard let conversationId: String = event.getEventInput(.conversationId),
+                      let accountId: String = event.getEventInput(.accountId) else { return }
+                guard let index = self?.conversationViewModels.firstIndex(where: { conversationModel in
+                    conversationModel.conversation.id == conversationId && conversationModel.conversation.accountId == accountId
+                }) else { return }
+                self?.conversationViewModels.remove(at: index)
+                self?.updateConversations()
+            })
+            .disposed(by: self.disposeBag)
+    }
+
+    private func subscribeSearch() {
+        searchModel?
+            .filteredResults
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] conversations in
+                guard let self = self else { return }
+                let filteredConv = conversations.isEmpty && searchQuery.isEmpty ? nil : conversations
+                self.updateConversations(with: filteredConv)
+            })
+            .disposed(by: self.disposeBag)
+
+        searchModel?
+            .temporaryConversation
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] conversation in
+                guard let self = self else { return }
+                withAnimation {
+                    self.temporaryConversation = conversation
+                }
+            })
+            .disposed(by: self.disposeBag)
+
+        searchModel?
+            .jamsTemporaryResults
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] conversations in
+                guard let self = self else { return }
+                withAnimation {
+                    self.jamsSearchResult = conversations
+                }
+            })
+            .disposed(by: self.disposeBag)
+        searchModel?
+            .searchStatus
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] status in
+                guard let self = self else { return }
+                self.updateSearchStatus(with: status)
+            })
+            .disposed(by: self.disposeBag)
+    }
+
+    private func updateConversations(with filtered: [ConversationViewModel]? = nil) {
+        DispatchQueue.main.async {[weak self] in
+            guard let self = self else { return }
+            withAnimation {
+                // Use filtered conversations if provided; otherwise, fall back to all conversationViewModels
+                self.conversations = filtered ?? self.conversationViewModels
+            }
+        }
+    }
+
+    func showConversation(withConversationViewModel conversationViewModel: ConversationViewModel) {
+        presentedConversation.updatePresentedConversation(conversationViewModel: conversationViewModel)
+        self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel:
+                                                                        conversationViewModel))
+    }
+
+    func showConversationFromQRCode(jamiId: String) {
+        // Ensure there is a current account available
+        guard let account = accountsService.currentAccount else { return }
+
+        // Attempt to find an existing one-to-one conversation with the specified jamiId
+        if let existingConversation = conversations.first(where: {
+            $0.conversation.type == .oneToOne && $0.conversation.getParticipants().first?.jamiId == jamiId
+        }) {
+            // Update and show the existing conversation
+            presentedConversation.updatePresentedConversation(conversationViewModel: existingConversation)
+            stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: existingConversation))
+            return
+        }
+
+        // Create a new temporary swarm conversation since no existing one matched
+        let tempConversation = createTemporarySwarmConversation(with: jamiId, accountId: account.id)
+        temporaryConversation = tempConversation
+        presentedConversation.updatePresentedConversation(conversationViewModel: tempConversation)
+        stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: tempConversation))
+    }
+
+    private func createTemporarySwarmConversation(with hash: String, accountId: String) -> ConversationViewModel {
+        let uri = JamiURI.init(schema: URIType.ring, infoHash: hash)
+        let conversation = ConversationModel(withParticipantUri: uri,
+                                             accountId: accountId)
+        conversation.type = .oneToOne
+        let newConversation = ConversationViewModel(with: self.injectionBag)
+        newConversation.userName.accept(hash)
+        newConversation.conversation = conversation
+        newConversation.isTemporary.accept(true)
+        return newConversation
+    }
+
+    func showConversationIfExists(conversationId: String) {
+        if let conversation = self.conversations.first(where: { conv in
+            conv.conversation.id == conversationId
+        }) {
+            self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: conversation))
+        }
+    }
+
+    func showDialpad() {
+        self.stateSubject.onNext(ConversationState.showDialpad(inCall: false))
+    }
+
+    func isSipAccount() -> Bool {
+        guard let account = self.accountsService.currentAccount else { return false }
+        return account.type == .sip
+    }
+
+    func showSipConversation(withNumber number: String) {
+        guard let account = self.accountsService
+                .currentAccount else {
+            return
+        }
+        let uri = JamiURI.init(schema: URIType.sip,
+                               infoHash: number,
+                               account: account)
+        let conversation = ConversationModel(withParticipantUri: uri,
+                                             accountId: account.id,
+                                             hash: number)
+        conversation.type = .sip
+        let newConversation = ConversationViewModel(with: self.injectionBag)
+        newConversation.conversation = conversation
+        self.stateSubject
+            .onNext(ConversationState
+                        .conversationDetail(conversationViewModel:
+                                                newConversation))
+    }
+
+    func deleteConversation(conversationViewModel: ConversationViewModel) {
+        conversationViewModel.closeAllPlayers()
+        let accountId = conversationViewModel.conversation.accountId
+        let conversationId = conversationViewModel.conversation.id
+        if conversationViewModel.conversation.isCoredialog(),
+           let participantId = conversationViewModel.conversation.getParticipants().first?.jamiId {
+            self.contactsService
+                .removeContact(withId: participantId,
+                               ban: false,
+                               withAccountId: accountId)
+                .asObservable()
+                .subscribe(onCompleted: { [weak self, weak conversationViewModel] in
+                    guard let conversationViewModel = conversationViewModel else { return }
+                    self?.conversationsService
+                        .removeConversationFromDB(conversation: conversationViewModel.conversation,
+                                                  keepConversation: false)
+                })
+                .disposed(by: self.disposeBag)
+        } else {
+            self.conversationsService.removeConversation(conversationId: conversationId, accountId: accountId)
+        }
+    }
+
+    func blockConversation(conversationViewModel: ConversationViewModel) {
+        conversationViewModel.closeAllPlayers()
+        let accountId = conversationViewModel.conversation.accountId
+        let conversationId = conversationViewModel.conversation.id
+        if conversationViewModel.conversation.isCoredialog(),
+           let participantId = conversationViewModel.conversation.getParticipants().first?.jamiId {
+            self.contactsService
+                .removeContact(withId: participantId,
+                               ban: true,
+                               withAccountId: accountId)
+                .asObservable()
+                .subscribe(onCompleted: { [weak self, weak conversationViewModel] in
+                    guard let conversationViewModel = conversationViewModel else { return }
+                    self?.conversationsService
+                        .removeConversationFromDB(conversation: conversationViewModel.conversation,
+                                                  keepConversation: false)
+                })
+                .disposed(by: self.disposeBag)
+        } else {
+            self.conversationsService.removeConversation(conversationId: conversationId, accountId: accountId)
+        }
+    }
+
+    // MARK: - PresentedConversation
+    struct PresentedConversation {
+        let temporaryConversationId = "temporary"
+        var presentedId: String = ""
+
+        mutating func updatePresentedConversation(conversationViewModel: ConversationViewModel) {
+            if conversationViewModel.conversation.id.isEmpty {
+                presentedId = temporaryConversationId
+            } else {
+                presentedId = conversationViewModel.conversation.id
+            }
+        }
+
+        func isTemporaryPresented() -> Bool {
+            return self.presentedId == temporaryConversationId
+        }
+
+        func hasPresentedConversation() -> Bool {
+            return !presentedId.isEmpty
+        }
+
+        mutating func resetPresentedConversation() {
+            self.presentedId = ""
+        }
+    }
+
+    var presentedConversation = PresentedConversation()
+
+    // MARK: - Navigation
+    enum Target {
+        case smartList
+        case newMessage
+    }
+
+    @Published var slideDirectionUp: Bool = true
+
+    @Published var navigationTarget: Target = .smartList
+
+    // MARK: - Search
+    func performSearch(query: String) {
+        withAnimation {
+            self.searchQuery = query
+        }
+        if let searchModel = self.searchModel {
+            searchModel.searchBarText.accept(query)
+        }
+    }
+
+    private func updateSearchStatus(with status: SearchStatus? = nil) {
+        if let status = status {
+            switch status {
+            case .searching:
+                searchStatus = status
+            default:
+                evaluateSearchResults()
+            }
+        } else {
+            evaluateSearchResults()
+        }
+    }
+
+    private func updateSearchStatusIfNeeded() {
+        guard let account = self.accountsService.currentAccount else { return }
+        if searchQuery.count > 2 || account.isJams {
+            evaluateSearchResults()
+        } else {
+            searchStatus = .invalidId
+        }
+    }
+
+    private func evaluateSearchResults() {
+        if temporaryConversation != nil {
+            searchStatus = .foundTemporary
+        } else if !jamsSearchResult.isEmpty {
+            searchStatus = .foundJams
+        } else {
+            searchStatus = .noResult
+        }
+    }
+
+    // MARK: - menu settings
+
+    func openSettings() {
+        self.stateSubject.onNext(ConversationState.showAccountSettings)
+    }
+
+    func createSwarm() {
+        self.stateSubject.onNext(ConversationState.createSwarm)
+    }
+
+    func scanQRCode() {
+        self.stateSubject.onNext(ConversationState.qrCode)
+    }
+
+    func showGeneralSettings() {
+        self.stateSubject.onNext(ConversationState.showGeneralSettings)
+    }
+
+    func openAboutJami() {
+        self.stateSubject.onNext(ConversationState.openAboutJami)
+    }
+
+    func donate() {
+        SharedActionsPresenter.openDonationLink()
+    }
+
+    func createAccount() {
+        self.stateSubject.onNext(ConversationState.createNewAccount)
+    }
+
+    var accountInfoToShare: String {
+        return self.accountsService.accountInfoToShare?.joined(separator: "\n") ?? ""
+    }
+
+    func closeAllPlayers() {
+        self.conversationViewModels.forEach { conversationModel in
+            conversationModel.closeAllPlayers()
+        }
+    }
+}
+// swiftlint:enable type_body_length
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/RequestsViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/RequestsViewModel.swift
new file mode 100644
index 0000000..2586409
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Models/RequestsViewModel.swift
@@ -0,0 +1,431 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import Foundation
+import SwiftUI
+import Combine
+import RxSwift
+import RxRelay
+
+enum RequestStatus {
+    case pending
+    case accepted
+    case refused
+    case banned
+
+    func toString() -> String {
+        switch self {
+        case .pending:
+            return ""
+        case .accepted:
+            return L10n.Invitations.accepted
+        case .refused:
+            return L10n.Invitations.refused
+        case .banned:
+            return L10n.Invitations.banned
+        }
+    }
+
+    func color() -> Color {
+        switch self {
+        case .pending:
+            return Color(UIColor.white)
+        case .accepted:
+            return Color(UIColor.systemGreen)
+        case .refused:
+            return Color(UIColor.orange)
+        case .banned:
+            return Color(UIColor.systemRed)
+        }
+    }
+}
+
+enum RequestAction {
+    case accept, discard, block
+}
+
+class RequestNameResolver: ObservableObject, Identifiable, Hashable {
+    var bestName: String = "" // Name to be shown in the request list. It is either the swarm title or the names of every participant in the conversation.
+    let id: String
+    /*
+     Name to be shown in the requests widget title on the smartList.
+     It is either the swarm title or the name of the first participant.
+     The request widget title is a name for every request,
+     separated by commas.
+     */
+    var requestName = BehaviorRelay(value: "")
+    let request: RequestModel
+    var registeredNames = [String: String]() // Dictionary of jamiId and registered name
+    let nameService: NameService
+    let disposeBag = DisposeBag()
+    var nameResolved = BehaviorRelay(value: false)
+
+    init(request: RequestModel, nameService: NameService) {
+        self.request = request
+        self.id = request.getIdentifier()
+        self.nameService = nameService
+        self.setName()
+    }
+
+    private func setName() {
+        if !request.name.isEmpty {
+            updateNameOnMainThread(with: request.name)
+            requestName.accept(request.name)
+            self.nameResolved.accept(true)
+        } else {
+            handleNoInvitationName()
+        }
+    }
+
+    private func updateNameOnMainThread(with name: String) {
+        DispatchQueue.main.async { [weak self] in
+            guard let self = self else { return }
+            self.bestName = name
+        }
+    }
+
+    private func updateNameFromRegistered() {
+        let newBestName = constructNameFromRegisteredNames()
+        updateNameOnMainThread(with: newBestName)
+        if let name = registeredNames.first {
+            requestName.accept(name.value.isEmpty ? name.key : name.value)
+        }
+    }
+
+    private func constructNameFromRegisteredNames() -> String {
+        return registeredNames.enumerated()
+            .map { _, element in
+                let (jamiId, name) = element
+                return name.isEmpty ? jamiId : name
+            }
+            .joined(separator: ", ")
+    }
+
+    private func handleNoInvitationName() {
+        initializeParticipantEntries()
+        updateNameFromRegistered()
+        performLookup()
+    }
+
+    private func initializeParticipantEntries() {
+        // Create a dictionary of participant IDs and names, so names can be updated when lookup is finished.
+        for participant in request.participants {
+            registeredNames[participant.jamiId] = ""
+        }
+    }
+
+    private func performLookup() {
+        for jamiId in registeredNames.keys {
+            lookupUserName(jamiId: jamiId)
+        }
+    }
+
+    private func lookupUserName(jamiId: String) {
+        nameService.usernameLookupStatus.asObservable()
+            .filter { lookupNameResponse in
+                return lookupNameResponse.address == jamiId
+            }
+            .take(1)
+            .subscribe(onNext: { lookupNameResponse in
+                if lookupNameResponse.state == .found && !lookupNameResponse.name.isEmpty {
+                    self.registeredNames[jamiId] = lookupNameResponse.name
+                    self.updateNameFromRegistered()
+                    self.nameResolved.accept(true)
+                }
+            })
+            .disposed(by: disposeBag)
+
+        nameService.lookupAddress(withAccount: request.accountId, nameserver: "", address: jamiId)
+    }
+
+    func getIdentifier() -> String {
+        return request.getIdentifier()
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
+
+    static func == (lhs: RequestNameResolver, rhs: RequestNameResolver) -> Bool {
+        return lhs.id == rhs.id
+    }
+}
+
+class RequestRowViewModel: ObservableObject, Identifiable, Hashable {
+    @Published var avatar: UIImage?
+    @Published var receivedDate: String
+    @Published var status: RequestStatus = .pending
+    let avatarSize: CGFloat = 55
+    let request: RequestModel
+    let id: String
+    let nameResolver: RequestNameResolver
+    let disposeBag = DisposeBag()
+
+    init(request: RequestModel, nameResolver: RequestNameResolver) {
+        self.nameResolver = nameResolver
+        self.id = request.getIdentifier()
+        self.request = request
+        self.receivedDate = request.receivedDate.conversationTimestamp()
+        self.setAvatar()
+        self.nameResolver.nameResolved
+            .startWith(self.nameResolver.nameResolved.value)
+            .subscribe(onNext: { [weak self] resolved in
+                if resolved {
+                    self?.setAvatar()
+                }
+            })
+            .disposed(by: disposeBag)
+    }
+
+    private func setAvatar() {
+        let newAvatar = createAvatar()
+        updateAvatarOnMainThread(with: newAvatar)
+    }
+
+    private func updateAvatarOnMainThread(with image: UIImage) {
+        DispatchQueue.main.async { [weak self] in
+            guard let self = self else { return }
+            self.avatar = image
+        }
+    }
+
+    private func createAvatar() -> UIImage {
+        if let avatarData = nameResolver.request.avatar, let image = UIImage(data: avatarData) {
+            return image
+        } else if request.type == .contact {
+            return UIImage.createContactAvatar(username: nameResolver.bestName, size: CGSize(width: avatarSize, height: avatarSize))
+        } else {
+            return UIImage.createSwarmAvatar(convId: nameResolver.request.conversationId, size: CGSize(width: avatarSize, height: avatarSize))
+        }
+    }
+
+    func requestAccepted() {
+        self.status = .accepted
+    }
+
+    func requestDiscarded() {
+        self.status = .refused
+    }
+
+    func requestBlocked() {
+        self.status = .banned
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
+
+    static func == (lhs: RequestRowViewModel, rhs: RequestRowViewModel) -> Bool {
+        return lhs.id == rhs.id
+    }
+
+}
+
+class RequestsViewModel: ObservableObject {
+    @Published var requestsRow = [RequestRowViewModel]()
+    @Published var requestNames = ""
+    @Published var unreadRequests = 0
+    @Published var requestViewOpened = false
+    var requestsNameResolvers = [RequestNameResolver]() // requests and resolved name
+    var title = L10n.Smartlist.invitationReceived
+
+    let requestsService: RequestsService
+    let conversationService: ConversationsService
+    let accountService: AccountsService
+    let contactsService: ContactsService
+    let presenceService: PresenceService
+    let injectionBar: InjectionBag
+    let nameService: NameService
+
+    let disposeBag = DisposeBag()
+    var titleDisposeBag = DisposeBag()
+
+    init(injectionBag: InjectionBag) {
+        self.requestsService = injectionBag.requestsService
+        self.conversationService = injectionBag.conversationsService
+        self.accountService = injectionBag.accountService
+        self.nameService = injectionBag.nameService
+        self.contactsService = injectionBag.contactsService
+        self.presenceService = injectionBag.presenceService
+        self.injectionBar = injectionBag
+        self.subscribeToNewRequests()
+    }
+
+    func subscribeToNewRequests() {
+        let conversationsStream = conversationService.conversations
+            .share()
+            .startWith(conversationService.conversations.value)
+
+        let requestsStream = requestsService.requests.asObservable()
+
+        let unhandledRequests = Observable.combineLatest(requestsStream, conversationsStream) {
+            [weak self] requests, conversations -> [RequestModel] in
+            guard let self = self, let account = self.accountService.currentAccount else {
+                return []
+            }
+            return self.filterRequestsNotInConversations(requests: requests, conversations: conversations, accountId: account.id)
+        }
+
+        unhandledRequests
+            .observe(on: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] newRequests in
+                self?.processNewRequests(newRequests)
+            })
+            .disposed(by: disposeBag)
+    }
+
+    private func filterRequestsNotInConversations(requests: [RequestModel], conversations: [ConversationModel], accountId: String) -> [RequestModel] {
+        let conversationIds = Set(conversations.map { $0.id })
+        return requests.filter { $0.accountId == accountId && !conversationIds.contains($0.conversationId) }
+    }
+
+    private func processNewRequests(_ newRequests: [RequestModel]) {
+        let newItems = self.findNewRequests(from: newRequests)
+        let outdatedItems = self.findOutdatedRequests(comparedTo: newRequests)
+
+        self.removeOutdatedItems(outdatedItems)
+        self.addNewRequests(newItems)
+        self.sortRequestsByReceivedDate()
+
+        self.updateUnreadCount()
+        if newItems.isEmpty && outdatedItems.isEmpty {
+            // Skip updating requestNames if there are no new or outdated items.
+            return
+        }
+        observeRequestNames()
+    }
+
+    private func findNewRequests(from newRequests: [RequestModel]) -> [RequestModel] {
+        return newRequests.filter { newItem in
+            !self.requestsNameResolvers.contains { $0.request.getIdentifier() == newItem.getIdentifier() }
+        }
+    }
+
+    private func findOutdatedRequests(comparedTo newRequests: [RequestModel]) -> [RequestNameResolver] {
+        return self.requestsNameResolvers.filter { oldItem in
+            !newRequests.contains { $0.getIdentifier() == oldItem.request.getIdentifier() }
+        }
+    }
+
+    private func removeOutdatedItems(_ items: [RequestNameResolver]) {
+        let identifiers = Set(items.map { $0.request.getIdentifier() })
+        self.requestsNameResolvers.removeAll { identifiers.contains($0.request.getIdentifier()) }
+    }
+
+    private func addNewRequests(_ newRequests: [RequestModel]) {
+        let newViewModels = newRequests.map { RequestNameResolver(request: $0, nameService: self.nameService) }
+        self.requestsNameResolvers.append(contentsOf: newViewModels)
+        if requestViewOpened {
+            for nameResolver in newViewModels {
+                requestsRow.append(RequestRowViewModel(request: nameResolver.request, nameResolver: nameResolver))
+            }
+        }
+    }
+
+    private func sortRequestsByReceivedDate() {
+        self.requestsNameResolvers.sort(by: { $0.request.receivedDate < $1.request.receivedDate })
+    }
+
+    private func updateUnreadCount() {
+        self.unreadRequests = self.requestsNameResolvers.count
+    }
+
+    private func observeRequestNames() {
+        self.titleDisposeBag = DisposeBag()
+
+        // Create a combined observable for request names
+        Observable.combineLatest(requestsNameResolvers.map { $0.requestName.asObservable() })
+            .map { names in names.joined(separator: ", ") }
+            .subscribe(onNext: { combinedNames in
+                DispatchQueue.main.async { [weak self] in
+                    self?.requestNames = combinedNames
+                }
+            })
+            .disposed(by: self.titleDisposeBag)
+    }
+
+    // MARK: - presenting requests list
+
+    func presentRequests() {
+        // When the list of requests is presented, maintain the same number of requests.
+        // Create rows from the current requests so that the list does not change as requests are processed.
+        generateRequestRows()
+        requestViewOpened.toggle()
+    }
+
+    func generateRequestRows() {
+        requestsRow = [RequestRowViewModel]()
+        for nameResolver in requestsNameResolvers {
+            requestsRow.append(RequestRowViewModel(request: nameResolver.request, nameResolver: nameResolver))
+        }
+    }
+
+    // MARK: - request actions
+
+    func accept(requestRow: RequestRowViewModel) {
+        processRequest(requestRow, action: .accept) {
+            if requestRow.request.isDialog(), let jamiId = requestRow.request.participants.first?.jamiId {
+                self.presenceService.subscribeBuddy(withAccountId: requestRow.request.accountId, withUri: jamiId, withFlag: true)
+            }
+        }
+    }
+
+    func discard(requestRow: RequestRowViewModel) {
+        processRequest(requestRow, action: .discard)
+    }
+
+    func block(requestRow: RequestRowViewModel) {
+        processRequest(requestRow, action: .block) {
+            guard let jamiId = requestRow.request.participants.first?.jamiId else { return }
+            self.removeContactAndBan(jamiId: jamiId, accountId: requestRow.request.accountId)
+        }
+    }
+
+    private func processRequest(_ requestRow: RequestRowViewModel, action: RequestAction, completion: (() -> Void)? = nil) {
+        let requestServiceAction = (action == .accept) ? requestsService.acceptConverversationRequest : requestsService.discardConverversationRequest
+
+        requestServiceAction(requestRow.request.conversationId, requestRow.request.accountId)
+            .subscribe(
+                onError: { error in
+                    print("Error processing request: \(error.localizedDescription)")
+                },
+                onCompleted: { [weak requestRow] in
+                    guard let requestRow = requestRow else { return }
+                    switch action {
+                    case .accept:
+                        requestRow.requestAccepted()
+                        completion?()
+                    case .discard:
+                        requestRow.requestDiscarded()
+                    case .block:
+                        requestRow.requestBlocked()
+                        completion?()
+                    }
+                }
+            )
+            .disposed(by: disposeBag)
+    }
+
+    private func removeContactAndBan(jamiId: String, accountId: String) {
+        contactsService.removeContact(withId: jamiId, ban: true, withAccountId: accountId)
+            .subscribe()
+            .disposed(by: disposeBag)
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/AccountLists.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/AccountLists.swift
new file mode 100644
index 0000000..f3689ea
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/AccountLists.swift
@@ -0,0 +1,128 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+
+struct AccountLists: View {
+    @ObservedObject var model: AccountsViewModel
+    var createAccountCallback: (() -> Void)
+    let verticalSpacing: CGFloat = 15
+    let maxHeight: CGFloat = 300
+    let cornerRadius: CGFloat = 16
+    let shadowRadius: CGFloat = 6
+    var body: some View {
+        VStack(spacing: 10) {
+            accountsView()
+            newAccountButton()
+        }
+        .padding(.horizontal, 5)
+    }
+
+    @ViewBuilder
+    private func accountsView() -> some View {
+        VStack {
+            Spacer()
+                .frame(height: verticalSpacing)
+            Text(model.headerTitle)
+                .fontWeight(.semibold)
+            Spacer()
+                .frame(height: verticalSpacing)
+            accountsList()
+            Spacer()
+                .frame(height: verticalSpacing)
+        }
+        .background(VisualEffect(style: .systemMaterial, withVibrancy: false))
+        .cornerRadius(cornerRadius)
+        .shadow(radius: shadowRadius)
+        .fixedSize(horizontal: false, vertical: true)
+    }
+
+    @ViewBuilder
+    private func newAccountButton() -> some View {
+        Button(action: {
+            createAccountCallback()
+        }, label: {
+            Text(L10n.Smartlist.addAccountButton)
+                .lineLimit(1)
+                .padding()
+                .frame(maxWidth: .infinity)
+                .background(VisualEffect(style: .systemChromeMaterial, withVibrancy: false))
+        })
+        .frame(minWidth: 100, maxWidth: .infinity)
+        .cornerRadius(cornerRadius)
+        .shadow(radius: shadowRadius)
+    }
+
+    @ViewBuilder
+    private func accountsList() -> some View {
+        ScrollView {
+            VStack {
+                ForEach(model.accountsRows, id: \.id) { accountRow in
+                    AccountRowView(accountRow: accountRow, model: model)
+                }
+            }
+            .frame(minHeight: 0, maxHeight: .infinity)
+        }
+        .frame(maxHeight: maxHeight)
+    }
+}
+
+struct AccountRowView: View {
+    @ObservedObject var accountRow: AccountRow
+    @ObservedObject var model: AccountsViewModel
+    let cornerRadius: CGFloat = 8
+    var body: some View {
+        HStack(spacing: 0) {
+            Image(uiImage: accountRow.avatar)
+                .resizable()
+                .frame(width: accountRow.dimensions.imageSize, height: accountRow.dimensions.imageSize)
+                .clipShape(Circle())
+            Spacer().frame(width: accountRow.dimensions.spacing)
+            VStack(alignment: .leading) {
+                Text(accountRow.bestName)
+                    .lineLimit(1)
+            }
+            Spacer()
+        }
+        .padding(.horizontal)
+        .padding(.vertical, accountRow.dimensions.spacing)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .contentShape(Rectangle())
+        .background(backgroundForAccountRow())
+        .onTapGesture {
+            model.changeCurrentAccount(accountId: accountRow.id)
+        }
+    }
+
+    private var isSelectedAccount: Bool {
+        accountRow.id == model.selectedAccount
+    }
+
+    @ViewBuilder
+    private func backgroundForAccountRow() -> some View {
+        Group {
+            if isSelectedAccount {
+                Color(UIColor.secondarySystemFill)
+                    .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
+                    .padding(.horizontal, 6)
+            }
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/ConversationsView.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/ConversationsView.swift
new file mode 100644
index 0000000..f187014
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/ConversationsView.swift
@@ -0,0 +1,222 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+
+@available(iOS 15.0, *)
+struct SwipeActionsModifier: ViewModifier {
+    enum ActiveAlert: Identifiable {
+        case block, delete
+        var id: Self { self }
+    }
+
+    let conversation: ConversationViewModel
+    let model: ConversationsViewModel
+    @SwiftUI.State private var activeAlert: ActiveAlert?
+
+    func body(content: Content) -> some View {
+        content
+            .swipeActions(edge: .trailing) {
+                swipeButton(for: .block, color: .red, title: L10n.Global.block)
+                swipeButton(for: .delete, color: .orange, title: L10n.Actions.deleteAction)
+            }
+            .alert(item: $activeAlert, content: alertForType)
+    }
+
+    private func swipeButton(for alertType: ActiveAlert, color: Color, title: String) -> some View {
+        Button {
+            activeAlert = alertType
+        } label: {
+            Text(title)
+        }
+        .tint(color)
+    }
+
+    private func alertForType(_ alertType: ActiveAlert) -> Alert {
+        switch alertType {
+        case .block:
+            return Alert(
+                title: Text(L10n.Global.blockContact),
+                message: Text(L10n.Alerts.confirmBlockContact),
+                primaryButton: .default(Text(L10n.Global.cancel)),
+                secondaryButton: .destructive(Text(L10n.Global.block), action: { model.blockConversation(conversationViewModel: conversation) })
+            )
+        case .delete:
+            return Alert(
+                title: Text(L10n.Alerts.confirmDeleteConversationTitle),
+                message: Text(L10n.Alerts.confirmDeleteConversation),
+                primaryButton: .default(Text(L10n.Global.cancel)),
+                secondaryButton: .destructive(Text(L10n.Actions.deleteAction), action: { model.deleteConversation(conversationViewModel: conversation) })
+            )
+        }
+    }
+}
+
+extension View {
+    @ViewBuilder
+    func conditionalSmartListSwipeActions(conversation: ConversationViewModel, model: ConversationsViewModel) -> some View {
+        if #available(iOS 15.0, *), model.navigationTarget == .smartList {
+            self.modifier(SwipeActionsModifier(conversation: conversation, model: model))
+        } else {
+            self
+        }
+    }
+}
+
+struct ConversationsView: View {
+    @ObservedObject var model: ConversationsViewModel
+    @SwiftUI.State private var searchText = ""
+    var body: some View {
+        ForEach(model.conversations) { conversation in
+            Button(action: {
+                model.showConversation(withConversationViewModel: conversation)
+            }) {
+                ConversationRowView(model: conversation)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .contentShape(Rectangle())
+            }
+            .listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 15))
+        }
+        .hideRowSeparator()
+        .navigationBarBackButtonHidden(true)
+    }
+}
+
+struct TempConversationsView: View {
+    @ObservedObject var model: ConversationsViewModel
+    var body: some View {
+        if let conversation = model.temporaryConversation {
+            Button(action: {
+                model.showConversation(withConversationViewModel: conversation)
+            }) {
+                ConversationRowView(model: conversation, withSeparator: false)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .contentShape(Rectangle())
+            }
+            .listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 15))
+        }
+    }
+}
+
+struct JamsSearchResultView: View {
+    @ObservedObject var model: ConversationsViewModel
+    var body: some View {
+        ForEach(model.jamsSearchResult) { conversation in
+            Button(action: {
+                model.showConversation(withConversationViewModel: conversation)
+            }) {
+                ConversationRowView(model: conversation, withSeparator: true)
+                    .contentShape(Rectangle())
+            }
+            .listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 15))
+        }
+    }
+}
+
+struct ConversationRowView: View {
+    @ObservedObject var model: ConversationViewModel
+    var withSeparator: Bool = true
+    var body: some View {
+        VStack(alignment: .leading, spacing: 10) {
+            HStack {
+                ZStack(alignment: .bottomTrailing) {
+                    if let image = model.avatar {
+                        Image(uiImage: image)
+                            .resizable()
+                            .aspectRatio(contentMode: .fill)
+                            .frame(width: 55, height: 55, alignment: .center)
+                            .clipShape(Circle())
+                    } else {
+                        Image(uiImage: model.getDefaultAvatar())
+                            .resizable()
+                            .frame(width: 55, height: 55, alignment: .center)
+                            .clipShape(Circle())
+                    }
+                    presenceIndicator
+                }
+                Spacer()
+                    .frame(width: 15)
+                VStack(alignment: .leading) {
+                    Text(model.name)
+                        .fontWeight(model.unreadMessages > 0 ? .bold : .regular)
+                        .lineLimit(1)
+                    if !model.lastMessage.isEmpty {
+                        Spacer()
+                            .frame(height: 5)
+                        HStack(alignment: .bottom, spacing: 4) {
+                            Text(model.lastMessageDate + " -")
+                                .fontWeight(.regular)
+                                .font(.footnote)
+                                .lineLimit(1)
+                            Text( model.lastMessage)
+                                .font(.footnote)
+                                .lineLimit(1)
+                        }
+                    } else if model.isSynchronizing {
+                        Spacer()
+                            .frame(height: 5)
+                        Text(L10n.Smartlist.inSynchronization)
+                            .italic()
+                            .font(.footnote)
+                            .lineLimit(1)
+                    }
+                }
+                Spacer()
+                if model.unreadMessages > 0 {
+                    Text("\(model.unreadMessages)")
+                        .fontWeight(.semibold)
+                        .font(.footnote)
+                        .padding(.vertical, 4)
+                        .padding(.horizontal, 8)
+                        .foregroundColor(Color.unreadMessageColorText)
+                        .background(Color.unreadMessageBackground)
+                        .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
+                }
+            }
+            if withSeparator {
+                Divider()
+                    .padding(.leading, 55)
+            }
+        }
+    }
+
+    private var presenceIndicator: some View {
+        Group {
+            switch model.presence {
+            case .connected:
+                presenceCircle(color: Color.onlinePresenceColor)
+            case .available:
+                presenceCircle(color: Color.availablePresenceColor)
+            default:
+                EmptyView()
+            }
+        }
+    }
+
+    private func presenceCircle(color: Color) -> some View {
+        Circle()
+            .fill(color)
+            .frame(width: 14, height: 14)
+            .overlay(
+                Circle().stroke(Color(UIColor.systemBackground), lineWidth: 2)
+            )
+            .offset(x: -1, y: -1)
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/RequestsView.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/RequestsView.swift
new file mode 100644
index 0000000..f83f5a7
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/RequestsView.swift
@@ -0,0 +1,199 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import Foundation
+import SwiftUI
+
+struct RequestsIndicatorView: View {
+    @ObservedObject var model: RequestsViewModel
+    private let iconSize: CGFloat = 25
+    private let cornerRadius: CGFloat = 12
+    private let padding: CGFloat = 15
+    private let badgePadding: CGFloat = 8
+    private let badgeCornerRadius: CGFloat = 5
+    private let verticalPaddingForBadge: CGFloat = 20
+    private let horizontalPaddingForBody: CGFloat = 20
+    private let verticalTextPadding: CGFloat = 5
+    private let foregroundColor: Color = Color(UIColor.systemBackground)
+
+    var body: some View {
+        HStack(spacing: horizontalPaddingForBody) {
+            icon
+            description
+            Spacer()
+            unreadCounter
+        }
+        .frame(maxWidth: .infinity)
+        .background(Color.jamiRequestsColor)
+        .cornerRadius(cornerRadius)
+    }
+
+    private var icon: some View {
+        Image(systemName: "envelope.badge")
+            .resizable()
+            .aspectRatio(contentMode: .fit)
+            .frame(width: iconSize, height: iconSize)
+            .foregroundColor(foregroundColor)
+            .padding(.leading, horizontalPaddingForBody)
+    }
+
+    private var description: some View {
+        VStack(alignment: .leading, spacing: verticalTextPadding) {
+            Text(model.title)
+                .lineLimit(1)
+                .foregroundColor(foregroundColor)
+            Text(model.requestNames)
+                .lineLimit(1)
+                .font(.footnote)
+                .foregroundColor(foregroundColor)
+        }
+    }
+
+    private var unreadCounter: some View {
+        Text("\(model.unreadRequests)")
+            .font(.footnote)
+            .fontWeight(.semibold)
+            .foregroundColor(Color.requestBadgeForeground)
+            .padding(.horizontal, badgePadding)
+            .padding(.vertical, 4)
+            .background(Color.requestsBadgeBackground)
+            .clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
+            .padding(.vertical, verticalPaddingForBadge)
+            .padding(.trailing, horizontalPaddingForBody)
+    }
+}
+
+struct RequestsView: View {
+    @ObservedObject var model: RequestsViewModel
+
+    var body: some View {
+        NavigationView {
+            VStack(spacing: 0) {
+                Text(model.title)
+                    .font(.headline)
+                    .foregroundColor(Color(UIColor.systemBackground))
+                    .padding(.vertical, 20)
+
+                requestsList
+            }
+            .background(Color.jamiRequestsColor.ignoresSafeArea())
+        }
+    }
+
+    private var requestsList: some View {
+        List(model.requestsRow) { request in
+            RequestsRowView(requestRow: request, nameResolver: request.nameResolver, listModel: model)
+                .listRowBackground(Color.jamiRequestsColor)
+                .hideRowSeparator()
+        }
+        .hideRowSeparator()
+        .listStyle(PlainListStyle())
+        .edgesIgnoringSafeArea(.all)
+        .background(Color.jamiRequestsColor)
+    }
+}
+
+struct RequestsRowView: View {
+    @ObservedObject var requestRow: RequestRowViewModel
+    @ObservedObject var nameResolver: RequestNameResolver
+    var listModel: RequestsViewModel
+
+    // Constants
+    private let actionIconSize: CGFloat = 20
+    private let spacerWidth: CGFloat = 15
+    private let spacerHeight: CGFloat = 20
+    private let buttonPadding: CGFloat = 10
+    private let dividerOpacity: Double = 0.1
+    private let cornerRadius: CGFloat = 12
+    private let foregroundColor: Color = Color(UIColor.systemBackground)
+
+    var body: some View {
+        VStack(spacing: 0) {
+            userInfoView
+            Spacer().frame(height: spacerHeight)
+            actionButtonsView
+            Spacer().frame(height: spacerHeight)
+            Divider()
+                .background(Color(UIColor.systemBackground).opacity(dividerOpacity))
+        }
+    }
+
+    private var userInfoView: some View {
+        HStack(alignment: .center) {
+            avatarView
+            Spacer().frame(width: spacerWidth)
+            VStack(alignment: .leading, spacing: 5) {
+                Text(nameResolver.bestName)
+                    .foregroundColor(foregroundColor)
+                    .lineLimit(1)
+                Text(requestRow.receivedDate)
+                    .font(.footnote)
+                    .foregroundColor(foregroundColor)
+            }
+            Spacer()
+            Text(requestRow.status.toString())
+                .font(.footnote)
+                .padding(.horizontal, buttonPadding)
+                .foregroundColor(requestRow.status.color())
+        }
+    }
+
+    private var avatarView: some View {
+        Group {
+            if let avatar = requestRow.avatar {
+                Image(uiImage: avatar)
+                    .resizable()
+                    .aspectRatio(contentMode: .fill)
+                    .frame(width: requestRow.avatarSize, height: requestRow.avatarSize)
+                    .clipShape(Circle())
+            }
+        }
+    }
+
+    private var actionButtonsView: some View {
+        HStack {
+            actionIcon("slash.circle") {
+                listModel.block(requestRow: requestRow)
+            }
+            Spacer().frame(width: spacerWidth)
+            actionIcon("xmark") {
+                listModel.discard(requestRow: requestRow)
+            }
+            Spacer().frame(width: spacerWidth)
+            actionIcon("checkmark") {
+                listModel.accept(requestRow: requestRow)
+            }
+        }
+    }
+
+    private func actionIcon(_ systemName: String, action: @escaping () -> Void) -> some View {
+        Image(systemName: systemName)
+            .resizable()
+            .aspectRatio(contentMode: .fit)
+            .frame(width: actionIconSize, height: actionIconSize)
+            .foregroundColor(Color.requestBadgeForeground)
+            .padding(.horizontal, buttonPadding)
+            .padding(.vertical, buttonPadding)
+            .frame(maxWidth: .infinity)
+            .background(Color.requestsBadgeBackground)
+            .cornerRadius(cornerRadius)
+            .onTapGesture(perform: action)
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SearchBar.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SearchBar.swift
new file mode 100644
index 0000000..1aa6c6c
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SearchBar.swift
@@ -0,0 +1,129 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+import UIKit
+import Combine
+
+public extension View {
+    func navigationBarSearch(_ searchText: Binding<String>, isActive: Binding<Bool>, isSearchBarDisabled: Binding<Bool>) -> some View {
+        return overlay(SearchBar(text: searchText, isActive: isActive, isSearchBarDisabled: isSearchBarDisabled).frame(width: 0, height: 0))
+    }
+}
+
+private struct SearchBar: UIViewControllerRepresentable {
+    @Binding var text: String
+    @Binding var isActive: Bool
+    @Binding var isSearchBarDisabled: Bool // used to programaticly dismiss search controller
+
+    init(text: Binding<String>, isActive: Binding<Bool>, isSearchBarDisabled: Binding<Bool>) {
+        self._text = text
+        self._isActive = isActive
+        self._isSearchBarDisabled = isSearchBarDisabled
+    }
+
+    func makeUIViewController(context: Context) -> SearchBarWrapperController {
+        return SearchBarWrapperController()
+    }
+
+    func updateUIViewController(_ controller: SearchBarWrapperController, context: Context) {
+        controller.searchController = context.coordinator.searchController
+        if self.isSearchBarDisabled {
+            controller.searchController?.isActive = false
+        }
+    }
+
+    func makeCoordinator() -> Coordinator {
+        return Coordinator(text: $text, isActive: $isActive, isSearchBarDisabled: $isSearchBarDisabled)
+    }
+
+    class Coordinator: NSObject, UISearchResultsUpdating, UISearchControllerDelegate {
+        @Binding var text: String
+        @Binding var isActive: Bool
+        @Binding var isSearchBarDisabled: Bool
+        let searchController: UISearchController
+
+        init(text: Binding<String>, isActive: Binding<Bool>, isSearchBarDisabled: Binding<Bool>) {
+            self._text = text
+            self._isActive = isActive
+            self._isSearchBarDisabled = isSearchBarDisabled
+            self.searchController = UISearchController(searchResultsController: nil)
+
+            super.init()
+
+            searchController.searchResultsUpdater = self
+            searchController.searchBar.searchTextField.addTarget(self, action: #selector(searchBarTextDidBeginEditing(_:)), for: .editingDidBegin)
+            searchController.searchBar.searchTextField.addTarget(self, action: #selector(searchBarTextDidEndEditing(_:)), for: .editingDidEnd)
+            searchController.hidesNavigationBarDuringPresentation = true
+            searchController.obscuresBackgroundDuringPresentation = false
+
+            self.searchController.searchBar.text = self.text
+            searchController.delegate = self
+        }
+
+        @objc private func searchBarTextDidBeginEditing(_ textField: UITextField) {
+            DispatchQueue.main.async {
+                withAnimation {
+                    self.isSearchBarDisabled = false
+                    self.isActive = true
+                }
+            }
+        }
+
+        func didDismissSearchController(_ searchController: UISearchController) {
+            DispatchQueue.main.async {
+                withAnimation {
+                    self.isActive = false
+                }
+            }
+        }
+
+        @objc private func searchBarTextDidEndEditing(_ textField: UITextField) {
+            DispatchQueue.main.async {
+                withAnimation {
+                    self.isActive = false
+                }
+            }
+        }
+
+        func updateSearchResults(for searchController: UISearchController) {
+            // DispatchQueue.main.async is important to avoid "Modifying state during view update, this will cause undefined behavior." error
+            DispatchQueue.main.async {
+                guard let text = searchController.searchBar.text else { return }
+                self.text = text
+            }
+        }
+    }
+
+    class SearchBarWrapperController: UIViewController {
+        var searchController: UISearchController? // {
+
+        override func viewWillAppear(_ animated: Bool) {
+            super.viewWillAppear(animated)
+            self.parent?.navigationItem.searchController = self.searchController
+            self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
+        }
+
+        override func viewDidAppear(_ animated: Bool) {
+            super.viewDidAppear(animated)
+            self.parent?.navigationItem.searchController = self.searchController
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContainer.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContainer.swift
new file mode 100644
index 0000000..41919a0
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContainer.swift
@@ -0,0 +1,312 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+import UIKit
+import Combine
+
+struct SmartListContainer: View {
+    @ObservedObject var model: ConversationsViewModel
+    var body: some View {
+        switch model.navigationTarget {
+        case .smartList:
+            SmartListView(model: model)
+        case .newMessage:
+            NewMessageView(model: model)
+                .applySlideTransition(directionUp: model.slideDirectionUp)
+        }
+    }
+}
+
+struct NewMessageView: View {
+    @ObservedObject var model: ConversationsViewModel
+    @SwiftUI.State private var isSearchBarActive = false // To track state initiated by the user
+    var body: some View {
+        PlatformAdaptiveNavView {
+            SearchableConversationsView(model: model, isSearchBarActive: $isSearchBarActive)
+                .navigationBarTitleDisplayMode(.inline)
+                .navigationTitle(L10n.Smartlist.newMessage)
+                .navigationBarItems(leading: Button(L10n.Global.cancel) {
+                    model.slideDirectionUp = false
+                    withAnimation {
+                        model.navigationTarget = .smartList
+                    }
+                })
+
+        }
+    }
+}
+
+struct SmartListView: View {
+    @ObservedObject var model: ConversationsViewModel
+    // account list presentation
+    @SwiftUI.State private var showAccountList = false
+    @SwiftUI.State private var coverBackgroundOpacity: CGFloat = 0
+    @SwiftUI.State private var isSearchBarActive = false // To track state initiated by the user
+    let maxCoverBackgroundOpacity: CGFloat = 0.09
+    let minCoverBackgroundOpacity: CGFloat = 0
+    @SwiftUI.State  var showingPicker = false
+    // share account info
+    @SwiftUI.State private var isSharing = false
+    var body: some View {
+        PlatformAdaptiveNavView {
+            ZStack(alignment: .bottom) {
+                SearchableConversationsView(model: model, isSearchBarActive: $isSearchBarActive)
+                    .navigationBarTitleDisplayMode(.inline)
+                    .navigationBarTitle("", displayMode: .inline)
+                    .navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems)
+                    .zIndex(0)
+                if showAccountList {
+                    backgroundCover()
+                    accountListsView()
+                }
+            }
+        }
+        .sheet(isPresented: $showingPicker) {
+            ContactPicker { contact in
+                model.showSipConversation(withNumber: contact)
+                showingPicker = false
+            }
+        }
+        .onChange(of: isSearchBarActive) { _ in
+            showAccountList = false
+        }
+    }
+
+    private var leadingBarItems: some View {
+        Button(action: {
+            toggleAccountList()
+        }) {
+            CurrentAccountButton(model: model.accountsModel)
+        }
+    }
+
+    @ViewBuilder
+    private func backgroundCover() -> some View {
+        Color(UIColor.black).opacity(coverBackgroundOpacity)
+            .ignoresSafeArea(edges: [.top, .bottom])
+            .allowsHitTesting(true)
+            .onTapGesture {
+                toggleAccountList()
+            }
+    }
+
+    @ViewBuilder
+    private func accountListsView() -> some View {
+        AccountLists(model: model.accountsModel) {
+            toggleAccountList()
+            model.createAccount()
+        }
+        .zIndex(1)
+        .transition(.move(edge: .bottom))
+        .animation(.easeOut, value: showAccountList)
+    }
+
+    private func toggleAccountList() {
+        setupBeforeTogglingAccountList()
+        animateAccountListVisibility()
+    }
+
+    private func setupBeforeTogglingAccountList() {
+        prepareAccountsIfNeeded()
+        updateCoverBackgroundOpacity()
+    }
+
+    // Update accounts if the list is about to be shown.
+    private func prepareAccountsIfNeeded() {
+        guard !showAccountList else { return }
+        model.accountsModel.getAccountsRows()
+    }
+
+    private func updateCoverBackgroundOpacity() {
+        coverBackgroundOpacity = showAccountList ? minCoverBackgroundOpacity : maxCoverBackgroundOpacity
+    }
+
+    private func animateAccountListVisibility() {
+        withAnimation {
+            showAccountList.toggle()
+        }
+    }
+
+    private var trailingBarItems: some View {
+        HStack {
+            if model.isSipAccount() {
+                menuButton
+                bookButton
+            } else {
+                menuButton
+                composeButton
+            }
+        }
+    }
+
+    private var bookButton: some View {
+        Button(action: { showingPicker.toggle() }) {
+            if let uiImage = UIImage(asset: Asset.phoneBook) {
+                Image(uiImage: uiImage)
+                    .foregroundColor(Color.jamiColor)
+            }
+        }
+    }
+
+    private var diapladButton: some View {
+        Button(action: { model.showDialpad() }) {
+            Image(systemName: "square.grid.3x3.topleft.filled")
+                .foregroundColor(Color.jamiColor)
+        }
+    }
+
+    private var menuButton: some View {
+        Menu {
+            if !model.isSipAccount() {
+                createSwarmButton
+                if #available(iOS 16.0, *) {
+                    shareLinkButton
+                }
+            }
+            accountsButton
+            settingsButton
+            generalSettingsButton
+            donateButton
+            aboutJamiButton
+        } label: {
+            Image(systemName: "ellipsis.circle")
+                .foregroundColor(Color.jamiColor)
+        }
+    }
+
+    private var composeButton: some View {
+        Button(action: triggerNewMessageAnimation) {
+            Image(systemName: "square.and.pencil")
+                .foregroundColor(Color.jamiColor)
+        }
+    }
+
+    private var createSwarmButton: some View {
+        Button(action: model.createSwarm) {
+            Label(L10n.Swarm.newSwarm, systemImage: "person.2")
+        }
+    }
+
+    @available(iOS 16.0, *)
+    private var shareLinkButton: some View {
+        ShareLink(item: model.accountInfoToShare) {
+            Label(L10n.Smartlist.inviteFriends, systemImage: "envelope.open")
+                .padding()
+                .background(Color.blue)
+                .foregroundColor(.white)
+                .cornerRadius(8)
+        }
+    }
+
+    private var accountsButton: some View {
+        Button(action: toggleAccountList) {
+            Label(L10n.Smartlist.accounts, systemImage: "list.bullet")
+        }
+    }
+
+    private var settingsButton: some View {
+        Button(action: model.openSettings) {
+            Label(L10n.Global.accountSettings, systemImage: "person.circle")
+        }
+    }
+
+    private var generalSettingsButton: some View {
+        Button(action: model.showGeneralSettings) {
+            Label(L10n.Global.advancedSettings, systemImage: "gearshape")
+        }
+    }
+
+    private var donateButton: some View {
+        Button(action: model.donate) {
+            Label(L10n.Global.donate, systemImage: "heart")
+        }
+    }
+
+    private var aboutJamiButton: some View {
+        Button(action: model.openAboutJami) {
+            Label {
+                Text(L10n.Smartlist.aboutJami)
+            } icon: {
+                Image(uiImage: model.jamiImage)
+            }
+        }
+    }
+
+    private func triggerNewMessageAnimation() {
+        model.slideDirectionUp = true
+        withAnimation {
+            model.navigationTarget = .newMessage
+        }
+    }
+}
+
+struct SearchableConversationsView: View {
+    @ObservedObject var model: ConversationsViewModel
+    @Binding var isSearchBarActive: Bool
+    @SwiftUI.State private var searchText = ""
+    @SwiftUI.State private var isSearchBarDisabled = false // To programmatically disable the search bar
+    @SwiftUI.State private var scrollViewOffset: CGFloat = 0
+    var body: some View {
+        SmartListContentView(model: model, mode: model.navigationTarget, requestsModel: model.requestsModel, isSearchBarActive: $isSearchBarActive)
+            .navigationBarSearch(self.$searchText, isActive: $isSearchBarActive, isSearchBarDisabled: $isSearchBarDisabled)
+            .onChange(of: searchText) { _ in
+                model.performSearch(query: searchText)
+            }
+            .onChange(of: model.conversationCreated) { _ in
+                if model.conversationCreated.isEmpty { return }
+                isSearchBarDisabled = true
+                searchText = ""
+            }
+    }
+}
+
+struct CurrentAccountButton: View {
+    @ObservedObject var model: AccountsViewModel
+    var body: some View {
+        HStack(spacing: 0) {
+            Image(uiImage: model.avatar)
+                .resizable()
+                .frame(width: model.dimensions.imageSize, height: model.dimensions.imageSize)
+                .clipShape(Circle())
+            Spacer()
+                .frame(width: model.dimensions.spacing)
+            VStack(alignment: .leading) {
+                Text(model.bestName)
+                    .bold()
+                    .lineLimit(1)
+                    .foregroundColor(Color.jamiColor)
+                    .frame(maxWidth: 150, alignment: .leading)
+            }
+            Spacer()
+        }
+        .transaction { transaction in
+            transaction.animation = nil
+        }
+    }
+}
+
+class CustomHostingController: UIHostingController<SmartListContainer> {
+
+    // Override supported interface orientations
+    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
+        return .all // Customize this based on your app's needs
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContentView.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContentView.swift
new file mode 100644
index 0000000..7b042d7
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/SmartListContentView.swift
@@ -0,0 +1,234 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+
+struct SmartListContentView: View {
+    @ObservedObject var model: ConversationsViewModel
+    @SwiftUI.State var mode: ConversationsViewModel.Target
+    @SwiftUI.State var hideTopView: Bool = true
+    @ObservedObject var requestsModel: RequestsViewModel
+    @Binding var isSearchBarActive: Bool
+    @SwiftUI.State var currentSearchBarStatus: Bool = false
+    @SwiftUI.State var isShowingScanner: Bool = false
+    @SwiftUI.State var isShowingNewMessageTop: Bool = true
+    var body: some View {
+        List {
+            publicDirectorySearchView
+            if !hideTopView {
+                if mode == .smartList {
+                    smartListTopView
+                        .transition(.opacity)
+                } else {
+                    newMessageTopView
+                        .transition(.opacity)
+                }
+            }
+            conversationsSearchHeaderView
+                .hideRowSeparator()
+            ConversationsView(model: model)
+        }
+        .onAppear {
+            // If there was an active search before presenting the conversation, the search results should remain the same upon returning to the page.
+            if model.presentedConversation.hasPresentedConversation() && !model.searchQuery.isEmpty {
+                isSearchBarActive = true
+                model.presentedConversation.resetPresentedConversation()
+            }
+            mode = model.navigationTarget
+            hideTopView = false
+        }
+        .onChange(of: isSearchBarActive) { _ in
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                withAnimation {
+                    isShowingNewMessageTop = !isSearchBarActive
+                }
+            }
+        }
+        .listStyle(.plain)
+        .hideRowSeparator()
+        .sheet(isPresented: $requestsModel.requestViewOpened) {
+            RequestsView(model: requestsModel)
+        }
+        .sheet(isPresented: $isShowingScanner) {
+            ScanView(onCodeScanned: { code in
+                model.showConversationFromQRCode(jamiId: code)
+                isShowingScanner = false
+            }, injectionBag: model.injectionBag)
+        }
+    }
+
+    @ViewBuilder private var smartListTopView: some View {
+        if  !isSearchBarActive && (requestsModel.unreadRequests > 0 || model.connectionState == .none) {
+            VStack {
+                if model.connectionState == .none {
+                    networkSettingsButton()
+                }
+                if requestsModel.unreadRequests > 0 {
+                    RequestsIndicatorView(model: requestsModel)
+                        .onTapGesture {
+                            requestsModel.presentRequests()
+                        }
+                }
+            }
+            .listRowInsets(EdgeInsets(top: 0, leading: 15, bottom: 5, trailing: 15))
+            .hideRowSeparator()
+        }
+    }
+
+    private func networkSettingsButton() -> some View {
+        HStack {
+            networkInfo()
+            Spacer()
+                .frame(width: 15)
+            Image(systemName: "gear")
+                .resizable()
+                .frame(width: 30, height: 30)
+                .foregroundColor(.white)
+        }
+        .padding(.horizontal, 15)
+        .padding(.vertical, 15)
+        .frame(maxWidth: .infinity)
+        .background(Color.networkAlertBackground)
+        .cornerRadius(12)
+        .onTapGesture {
+            openSettings()
+        }
+    }
+
+    private func networkInfo() -> some View {
+        VStack(spacing: 5) {
+            Text(L10n.Smartlist.noNetworkConnectivity)
+                .multilineTextAlignment(.center)
+                .foregroundColor(.white)
+            Text(L10n.Smartlist.cellularAccess)
+                .font(.footnote)
+                .multilineTextAlignment(.center)
+                .foregroundColor(.white)
+        }
+    }
+
+    private func openSettings() {
+        if let url = URL(string: UIApplication.openSettingsURLString) {
+            UIApplication.shared.open(url, completionHandler: nil)
+        }
+    }
+
+    @ViewBuilder private var newMessageTopView: some View {
+        if !isSearchBarActive {
+            VStack {
+                if isShowingNewMessageTop {
+                    HStack {
+                        actionItem(icon: "qrcode", title: L10n.Smartlist.newContact, action: { isShowingScanner.toggle() })
+                        Spacer()
+                        actionItem(icon: "person.2", title: L10n.Smartlist.newSwarm, action: model.createSwarm)
+                    }
+                    .hideRowSeparator()
+                }
+            }
+            .listRowInsets(EdgeInsets(top: 0, leading: 15, bottom: 5, trailing: 15))
+            .hideRowSeparator()
+        }
+    }
+
+    private func actionItem(icon: String, title: String, action: @escaping () -> Void) -> some View {
+        HStack {
+            Image(systemName: icon)
+                .resizable()
+                .aspectRatio(contentMode: .fit)
+                .frame(width: 18, height: 18)
+                .foregroundColor(.jamiColor)
+            Text(title)
+                .font(.callout)
+        }
+        .padding(.horizontal)
+        .padding(.vertical, 10)
+        .frame(maxWidth: .infinity)
+        .background(Color.jamiTertiaryControl)
+        .cornerRadius(12)
+        .onTapGesture(perform: action)
+    }
+
+    @ViewBuilder private var conversationsSearchHeaderView: some View {
+        if !model.searchQuery.isEmpty {
+            Text(L10n.Smartlist.conversations)
+                .fontWeight(.semibold)
+                .hideRowSeparator()
+            if model.conversations.isEmpty {
+                Text(L10n.Smartlist.noConversationsFound)
+                    .font(.callout)
+                    .hideRowSeparator()
+            }
+        }
+    }
+
+    @ViewBuilder private var publicDirectorySearchView: some View {
+        if isSearchBarActive && !model.searchQuery.isEmpty {
+            Text(model.publicDirectoryTitle)
+                .fontWeight(.semibold)
+                .hideRowSeparator()
+            searchResultView
+                .hideRowSeparator()
+        }
+    }
+
+    @ViewBuilder private var searchResultView: some View {
+        switch model.searchStatus {
+        case .foundTemporary:
+            tempConversationsView
+                .hideRowSeparator()
+        case .foundJams:
+            jamsSearchResultContainerView
+        case .searching:
+            searchingView
+        case .noResult, .invalidId:
+            noResultView
+                .hideRowSeparator()
+        case .notSearching:
+            EmptyView()
+        }
+    }
+
+    private var searchingView: some View {
+        VStack {
+            HStack {
+                Spacer()
+                SwiftUI.ProgressView()
+                Spacer()
+            }
+        }
+    }
+
+    private var tempConversationsView: some View {
+        VStack(alignment: .leading) {
+            TempConversationsView(model: model)
+        }
+    }
+
+    private var jamsSearchResultContainerView: some View {
+        JamsSearchResultView(model: model)
+    }
+
+    private var noResultView: some View {
+        VStack(alignment: .leading) {
+            Text(model.searchStatus.toString())
+                .font(.callout)
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/UIControllersWrappers.swift b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/UIControllersWrappers.swift
new file mode 100644
index 0000000..a118121
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SwiftUI/Views/UIControllersWrappers.swift
@@ -0,0 +1,117 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import SwiftUI
+import AVFoundation
+import ContactsUI
+
+struct ScanView: UIViewControllerRepresentable {
+    var onCodeScanned: (String) -> Void
+    let injectionBag: InjectionBag
+
+    typealias UIViewControllerType = ScanViewController
+
+    func makeUIViewController(context: Context) -> ScanViewController {
+        let viewController = ScanViewController.instantiate(with: self.injectionBag)
+        viewController.onCodeScanned = onCodeScanned
+        return viewController
+    }
+
+    func updateUIViewController(_ uiViewController: ScanViewController, context: Context) {
+    }
+
+    static func dismantleUIViewController(_ uiViewController: ScanViewController, coordinator: ()) {
+    }
+}
+
+struct ContactPicker: UIViewControllerRepresentable {
+    @Environment(\.presentationMode) var presentationMode
+    var onSelectContact: (String) -> Void
+
+    func makeUIViewController(context: Context) -> CNContactPickerViewController {
+        let picker = CNContactPickerViewController()
+        picker.delegate = context.coordinator
+        return picker
+    }
+
+    func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator(self, onSelectContact: onSelectContact)
+    }
+
+    class Coordinator: NSObject, CNContactPickerDelegate {
+        var parent: ContactPicker
+        var onSelectContact: (String) -> Void
+
+        init(_ parent: ContactPicker, onSelectContact: @escaping (String) -> Void) {
+            self.parent = parent
+            self.onSelectContact = onSelectContact
+        }
+
+        func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
+            let phoneNumbers = contact.phoneNumbers.map { $0.value.stringValue }
+            if phoneNumbers.isEmpty {
+                DispatchQueue.main.async { [weak self] in
+                    // No numbers available
+                    let alert = UIAlertController(title: L10n.Smartlist.noNumber,
+                                                  message: nil,
+                                                  preferredStyle: .alert)
+                    let cancelAction = UIAlertAction(title: L10n.Global.ok,
+                                                     style: .default) { (_: UIAlertAction!) -> Void in }
+                    alert.addAction(cancelAction)
+                    if let rootViewController = UIApplication.shared.windows.first?.rootViewController {
+                        rootViewController.present(alert, animated: true, completion: nil)
+                    }
+                    self?.parent.presentationMode.wrappedValue.dismiss()
+                }
+            } else if phoneNumbers.count == 1 {
+                DispatchQueue.main.async { [weak self] in
+                    self?.onSelectContact(phoneNumbers[0])
+                }
+            } else {
+                self.presentNumberSelection(from: picker, with: phoneNumbers)
+            }
+        }
+
+        private func presentNumberSelection(from picker: UIViewController, with numbers: [String]) {
+            DispatchQueue.main.async {
+                let alert = UIAlertController(title: L10n.Smartlist.selectOneNumber, message: nil, preferredStyle: .alert)
+                numbers.forEach { number in
+                    alert.addAction(UIAlertAction(title: number, style: .default, handler: { _ in
+                        DispatchQueue.main.async {
+                            self.onSelectContact(number)
+                        }
+                    }))
+                }
+                alert.addAction(UIAlertAction(title: L10n.Global.cancel, style: .cancel, handler: nil))
+                if let rootViewController = UIApplication.shared.windows.first?.rootViewController {
+                    rootViewController.present(alert, animated: true, completion: nil)
+                }
+            }
+        }
+
+        func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
+            DispatchQueue.main.async { [weak self] in
+                self?.parent.presentationMode.wrappedValue.dismiss()
+            }
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
index 8a74b41..8c173de 100644
--- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
+++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
@@ -41,7 +41,7 @@
     var showSearchResult: Bool = true
 
     func configure(with injectionBag: InjectionBag, source: FilterConversationDataSource, isIncognito: Bool, delegate: FilterConversationDelegate?) {
-        self.viewModel = JamiSearchViewModel(with: injectionBag, source: source)
+        self.viewModel = JamiSearchViewModel(with: injectionBag, source: source, searchOnlyExistingConversations: true)
         self.viewModel.setDelegate(delegate: delegate)
         self.isIncognito = isIncognito
         self.setUpView()
diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
index 4250270..155f7aa 100644
--- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
+++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
@@ -26,8 +26,11 @@
 
 enum SearchStatus {
     case notSearching
+    case foundTemporary
+    case foundJams
     case searching
     case noResult
+    case invalidId
 
     func toString() -> String {
         switch self {
@@ -36,7 +39,13 @@
         case .searching:
             return L10n.Global.search
         case .noResult:
-            return L10n.Smartlist.noResults
+            return "Username not found"
+        case .invalidId:
+            return "Invalid id"
+        case .foundTemporary:
+            return ""
+        case .foundJams:
+            return ""
         }
     }
 }
@@ -92,22 +101,25 @@
      Existing conversations with the title containing search result or one of
      the participant's name containing search result.
      */
-    private var filteredResults = BehaviorRelay(value: [ConversationViewModel]())
+    var filteredResults = BehaviorRelay(value: [ConversationViewModel]())
 
     // Jams temporary conversations created when perform search for a new contact
-    private let jamsTemporaryResults = BehaviorRelay<[ConversationViewModel]>(value: [])
+    let jamsTemporaryResults = BehaviorRelay<[ConversationViewModel]>(value: [])
 
     let searchBarText = BehaviorRelay<String>(value: "")
     var isSearching: Observable<Bool>!
     var searchStatus = PublishSubject<SearchStatus>()
     private let dataSource: FilterConversationDataSource
+    // Indicates if the search should be limited to only existing conversations.
+    private let searchOnlyExistingConversations: Bool
     private weak var delegate: FilterConversationDelegate?
 
-    init(with injectionBag: InjectionBag, source: FilterConversationDataSource) {
+    init(with injectionBag: InjectionBag, source: FilterConversationDataSource, searchOnlyExistingConversations: Bool) {
         self.nameService = injectionBag.nameService
         self.accountsService = injectionBag.accountService
         self.injectionBag = injectionBag
         self.dataSource = source
+        self.searchOnlyExistingConversations = searchOnlyExistingConversations
 
         // Observes if the user is searching.
         self.isSearching = searchBarText.asObservable()
@@ -121,6 +133,13 @@
             .observe(on: MainScheduler.instance)
             .distinctUntilChanged()
             .subscribe(onNext: { [weak self] text in
+                guard let account = self?.accountsService.currentAccount else { return }
+                if text.isEmpty {
+                    self?.searchStatus.onNext(.notSearching)
+                }
+                if text.count < 3 && !account.isJams {
+                    self?.searchStatus.onNext(.invalidId)
+                }
                 self?.search(withText: text)
             })
             .disposed(by: disposeBag)
@@ -133,7 +152,7 @@
     }
 
     func updateSearchStatus() {
-        if self.filteredResults.value.isEmpty && self.jamsTemporaryResults.value.isEmpty && self.temporaryConversation.value == nil {
+        if self.jamsTemporaryResults.value.isEmpty && self.temporaryConversation.value == nil {
             self.searchStatus.onNext(.noResult)
         } else {
             self.searchStatus.onNext(.notSearching)
@@ -227,6 +246,10 @@
         if let filteredConversations = getFilteredConversations(for: searchQuery) {
             self.filteredResults.accept(filteredConversations)
         }
+        // not need to searh on network
+        if searchOnlyExistingConversations {
+            return
+        }
         self.addTemporaryConversationsIfNeed(searchQuery: searchQuery)
     }
 
@@ -347,6 +370,7 @@
             newConversation.userName.accept(hash)
         }
         newConversation.conversation = conversation
+        newConversation.isTemporary.accept(true)
         return newConversation
     }
 
@@ -370,6 +394,7 @@
         let newConversation = ConversationViewModel(with: injectionBag,
                                                     conversation: conversation,
                                                     user: user)
+        newConversation.isTemporary.accept(true)
         return newConversation
     }
 
@@ -378,4 +403,5 @@
             delegate.showConversation(withConversationViewModel: conversation)
         }
     }
+
 }
diff --git a/Ring/Ring/Models/RequestModel.swift b/Ring/Ring/Models/RequestModel.swift
index bd27e80..c6ea820 100644
--- a/Ring/Ring/Models/RequestModel.swift
+++ b/Ring/Ring/Models/RequestModel.swift
@@ -66,8 +66,6 @@
             }
             if let name = profile.alias {
                 self.name = name
-            } else {
-                self.name = jamiId
             }
         }
     }
@@ -87,8 +85,6 @@
                 }
                 if let name = profile.alias {
                     self.name = name
-                } else {
-                    self.name = jamiId
                 }
             }
             if let receivedDateString = dictionary[RequestKey.received.rawValue],
@@ -109,6 +105,10 @@
            let conversationType = ConversationType(rawValue: typeInt) {
             self.conversationType = conversationType
         }
+
+        if self.conversationType == .nonSwarm {
+            self.type = .contact
+        }
         if let conversationId = dictionary[RequestKey.conversationId.rawValue] {
             self.conversationId = conversationId
         }
@@ -134,6 +134,14 @@
         self.conversationId = conversationId
     }
 
+    func getIdentifier() -> String {
+        if self.type == .conversation {
+            return conversationId
+        } else {
+            return self.participants.first?.jamiId ?? ""
+        }
+    }
+
     func isCoredialog() -> Bool {
         return self.conversationType == .nonSwarm || self.conversationType == .oneToOne
     }
diff --git a/Ring/Ring/Protocols/ConversationNavigation.swift b/Ring/Ring/Protocols/ConversationNavigation.swift
index b1394f7..6ffaa7d 100644
--- a/Ring/Ring/Protocols/ConversationNavigation.swift
+++ b/Ring/Ring/Protocols/ConversationNavigation.swift
@@ -217,7 +217,7 @@
         conversationViewController.viewModel = conversationViewModel
         self.present(viewController: conversationViewController,
                      withStyle: .show,
-                     withAnimation: true,
+                     withAnimation: false,
                      withStateable: conversationViewController.viewModel,
                      lockWhilePresenting: VCType.conversation.rawValue)
     }
diff --git a/Ring/Ring/QRCode/ScanViewController.swift b/Ring/Ring/QRCode/ScanViewController.swift
index a6550d4..af3799a 100644
--- a/Ring/Ring/QRCode/ScanViewController.swift
+++ b/Ring/Ring/QRCode/ScanViewController.swift
@@ -32,6 +32,7 @@
     @IBOutlet weak var bottomMarginTitleConstraint: NSLayoutConstraint!
     @IBOutlet weak var bottomCloseButtonConstraint: NSLayoutConstraint!
     let disposeBag = DisposeBag()
+    var onCodeScanned: ((String) -> Void)?
 
     // MARK: variables
     let systemSoundId: SystemSoundID = 1016
@@ -180,8 +181,7 @@
             if jamiId.isSHA1() {
                 AudioServicesPlayAlertSound(systemSoundId)
                 print("jamiId : " + jamiId)
-                self.dismiss(animated: true, completion: nil)
-                self.viewModel.openConversation(jamiId: jamiId)
+                onCodeScanned?(jamiId)
                 self.scannedQrCode = true
             } else {
                 let alert = UIAlertController(title: L10n.Scan.badQrCode, message: "", preferredStyle: .alert)
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jami.colorset/Contents.json
similarity index 76%
rename from Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json
rename to Ring/Ring/Resources/Colors.xcassets/jami.colorset/Contents.json
index 7cc755c..2c2a1a1 100644
--- a/Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json
+++ b/Ring/Ring/Resources/Colors.xcassets/jami.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "153",
-          "green" : "86",
-          "red" : "0"
+          "blue" : "0x5F",
+          "green" : "0x34",
+          "red" : "0x00"
         }
       },
       "idiom" : "universal"
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "153",
-          "green" : "86",
-          "red" : "0"
+          "blue" : "0.769",
+          "green" : "0.612",
+          "red" : "0.012"
         }
       },
       "idiom" : "universal"
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiButtonDark.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiButtonDark.colorset/Contents.json
index 91035b5..a252ba7 100644
--- a/Ring/Ring/Resources/Colors.xcassets/jamiButtonDark.colorset/Contents.json
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiButtonDark.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.600",
-          "green" : "0.337",
-          "red" : "0.000"
+          "blue" : "0x99",
+          "green" : "0x55",
+          "red" : "0x00"
         }
       },
       "idiom" : "universal"
@@ -20,12 +20,12 @@
         }
       ],
       "color" : {
-        "color-space" : "srgb",
+        "color-space" : "extended-linear-srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.914",
-          "green" : "0.725",
-          "red" : "0.012"
+          "blue" : "0xCF",
+          "green" : "0x7B",
+          "red" : "0x00"
         }
       },
       "idiom" : "universal"
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiButtonLight.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiButtonLight.colorset/Contents.json
index 9d46483..15fb9e6 100644
--- a/Ring/Ring/Resources/Colors.xcassets/jamiButtonLight.colorset/Contents.json
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiButtonLight.colorset/Contents.json
@@ -5,9 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.788",
-          "green" : "0.443",
-          "red" : "0.000"
+          "blue" : "0xC8",
+          "green" : "0x70",
+          "red" : "0x00"
         }
       },
       "idiom" : "universal"
@@ -23,9 +23,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.769",
-          "green" : "0.612",
-          "red" : "0.012"
+          "blue" : "0xC4",
+          "green" : "0x9C",
+          "red" : "0x03"
         }
       },
       "idiom" : "universal"
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiPrimaryControl.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiPrimaryControl.colorset/Contents.json
new file mode 100644
index 0000000..083927f
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiPrimaryControl.colorset/Contents.json
@@ -0,0 +1,56 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "153",
+          "green" : "86",
+          "red" : "0"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x29",
+          "green" : "0x17",
+          "red" : "0x00"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xCF",
+          "green" : "0x7B",
+          "red" : "0x00"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiRequestsColor.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiRequestsColor.colorset/Contents.json
new file mode 100644
index 0000000..775615d
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiRequestsColor.colorset/Contents.json
@@ -0,0 +1,54 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "gray-gamma-22",
+        "components" : {
+          "alpha" : "1.000",
+          "white" : "0xFF"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "color" : {
+        "color-space" : "display-p3",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x3A",
+          "green" : "0x21",
+          "red" : "0x08"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.942",
+          "green" : "0.896",
+          "red" : "0.826"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiSecondaryControl.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiSecondaryControl.colorset/Contents.json
new file mode 100644
index 0000000..2805f62
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiSecondaryControl.colorset/Contents.json
@@ -0,0 +1,56 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xC8",
+          "green" : "0x70",
+          "red" : "0x00"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xC8",
+          "green" : "0x70",
+          "red" : "0x00"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xC4",
+          "green" : "0x9C",
+          "red" : "0x03"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/Colors.xcassets/donationBanner.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiTertiaryControl.colorset/Contents.json
similarity index 100%
rename from Ring/Ring/Resources/Colors.xcassets/donationBanner.colorset/Contents.json
rename to Ring/Ring/Resources/Colors.xcassets/jamiTertiaryControl.colorset/Contents.json
diff --git a/Ring/Ring/Resources/Colors.xcassets/requestBadgeForeground.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/requestBadgeForeground.colorset/Contents.json
new file mode 100644
index 0000000..e01a411
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/requestBadgeForeground.colorset/Contents.json
@@ -0,0 +1,56 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "display-p3",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xFF",
+          "green" : "0xFF",
+          "red" : "0xFF"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "color" : {
+        "color-space" : "display-p3",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xFF",
+          "green" : "0xFF",
+          "red" : "0xFF"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "display-p3",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x99",
+          "green" : "0x56",
+          "red" : "0x00"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/Colors.xcassets/requestsBadgeBackground.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/requestsBadgeBackground.colorset/Contents.json
new file mode 100644
index 0000000..bcbc05c
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/requestsBadgeBackground.colorset/Contents.json
@@ -0,0 +1,56 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "1.000",
+          "green" : "1.000",
+          "red" : "1.000"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "color" : {
+        "color-space" : "display-p3",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x94",
+          "green" : "0x55",
+          "red" : "0x23"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xDA",
+          "green" : "0xC2",
+          "red" : "0xA3"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings
index 8686759..353b3f6 100644
--- a/Ring/Ring/Resources/en.lproj/Localizable.strings
+++ b/Ring/Ring/Resources/en.lproj/Localizable.strings
@@ -84,7 +84,13 @@
 // Smartlist
 "smartlist.yesterday" = "Yesterday";
 "smartlist.results" = "Public Directory";
+"smartlist.jamsResults" = "Search Result";
 "smartlist.conversations" = "Conversations";
+"smartlist.noConversationsFound" = "No conversations match your search";
+"smartlist.newContact" = "New Contact";
+"smartlist.newSwarm" = "New Swarm";
+"smartlist.accounts" = "Accounts";
+"smartlist.invitationReceived" = "Invitations received";
 "smartlist.noResults" = "No results";
 "smartlist.noConversation" = "No conversations";
 "smartlist.searchBarPlaceholder" = "Enter name...";
@@ -101,8 +107,13 @@
 "smartlist.accounts" = "Account list";
 "smartlist.disableDonation" = "Not now";
 "smartlist.donationExplanation" = "If you enjoy using Jami and believe in our mission, would you make a donation?";
+"smartlist.inSynchronization" = "conversation in synchronization";
+"smartlist.newMessage" = "New Message";
 
 //Conversation
+"conversation.addToContactsButton" = "Add to Contacts";
+"conversation.addToContactsLabel" = "Add to contacts?";
+"conversation.notContactLabel" = "is not in your contact list";
 "conversation.messagePlaceholder" = "Write to";
 "conversation.explanationSendingLocationTo" = "You are currently sharing your location with ";
 "conversation.explanationReceivingLocationFrom" = "You are currently receiving a live location from ";
@@ -123,6 +134,11 @@
 
 //Invitations
 "invitations.noInvitations" = "No invitations";
+"invitations.pending" = "pending";
+"invitations.accepted" = "accepted";
+"invitations.refused" = "refused";
+"invitations.banned" = "banned";
+"invitations.list" = "Invitations received";
 
 // Walkthrough
 
diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift
index 1193fd6..22a95ed 100644
--- a/Ring/Ring/Services/AccountsService.swift
+++ b/Ring/Ring/Services/AccountsService.swift
@@ -163,7 +163,7 @@
         }
     }
 
-    var accountInfoToShare: [Any]? {
+    var accountInfoToShare: [String]? {
         var info = [String]()
         guard let account = self.currentAccount else { return nil }
         var nameToContact = ""
diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift
index f45ef7c..e95517f 100644
--- a/Ring/Ring/Services/ConversationsService.swift
+++ b/Ring/Ring/Services/ConversationsService.swift
@@ -110,12 +110,12 @@
                 self?.sortAndUpdate(conversations: &currentConversations)
                 // load one message for each swarm conversation
                 for swarmId in conversationToLoad {
-                    self?.conversationsAdapter.loadConversationMessages(accountId, conversationId: swarmId, from: "", size: 1)
+                    self?.loadConversationMessages(conversationId: swarmId, accountId: accountId, from: "", size: 1)
                 }
             }, onError: { [weak self] _ in
                 self?.conversations.accept(currentConversations)
                 for swarmId in conversationToLoad {
-                    self?.conversationsAdapter.loadConversationMessages(accountId, conversationId: swarmId, from: "", size: 1)
+                    self?.loadConversationMessages(conversationId: swarmId, accountId: accountId, from: "", size: 1)
                 }
             })
             .disposed(by: self.disposeBag)
@@ -173,10 +173,6 @@
                 conversation.updatePreferences(preferences: prefsInfo)
             }
             conversation.addParticipantsFromArray(participantsInfo: participantsInfo, accountURI: accountURI)
-            if let lastRead = conversation.getLastReadMessage() {
-                let unreadInteractions = conversationsAdapter.countInteractions(accountId, conversationId: conversationId, from: lastRead, to: "", authorUri: accountURI)
-                conversation.numberOfUnreadMessages.accept(Int(unreadInteractions))
-            }
             conversations.append(conversation)
         }
     }
@@ -226,8 +222,10 @@
 
     // MARK: swarm interactions management
 
-    func loadConversationMessages(conversationId: String, accountId: String, from: String) {
-        self.conversationsAdapter.loadConversationMessages(accountId, conversationId: conversationId, from: from, size: 40)
+    func loadConversationMessages(conversationId: String, accountId: String, from: String, size: Int = 40) {
+        DispatchQueue.global(qos: .background).async {
+            self.conversationsAdapter.loadConversationMessages(accountId, conversationId: conversationId, from: from, size: size)
+        }
     }
 
     func loadMessagesUntil(messageId: String, conversationId: String, accountId: String, from: String) {
@@ -376,7 +374,7 @@
             data[ConversationNotificationsKeys.conversationId.rawValue] = conversationId
             data[ConversationNotificationsKeys.accountId.rawValue] = accountId
             NotificationCenter.default.post(name: NSNotification.Name(ConversationNotifications.conversationReady.rawValue), object: nil, userInfo: data)
-            self.conversationsAdapter.loadConversationMessages(accountId, conversationId: conversationId, from: "", size: 2)
+            self.loadConversationMessages(conversationId: conversationId, accountId: accountId, from: "", size: 2)
             self.sortIfNeeded()
             self.conversationReady.accept(conversationId)
             return
@@ -392,7 +390,7 @@
                 let unreadInteractions = conversationsAdapter.countInteractions(accountId, conversationId: conversationId, from: lastRead, to: "", authorUri: accountURI)
                 conversation.numberOfUnreadMessages.accept(Int(unreadInteractions))
             }
-            self.conversationsAdapter.loadConversationMessages(accountId, conversationId: conversationId, from: "", size: 1)
+            self.loadConversationMessages(conversationId: conversationId, accountId: accountId, from: "", size: 2)
             self.sortIfNeeded()
         }
         self.conversationReady.accept(conversationId)
diff --git a/Ring/RingTests/JamiSearchViewModelTests.swift b/Ring/RingTests/JamiSearchViewModelTests.swift
index 77ede1f..c5e9645 100644
--- a/Ring/RingTests/JamiSearchViewModelTests.swift
+++ b/Ring/RingTests/JamiSearchViewModelTests.swift
@@ -77,7 +77,7 @@
         conversationVM = ConversationViewModel(with: injectionBag)
         conversationVM.conversation = ConversationModel()
         dataSource = TestableFilteredDataSource(conversations: [conversationVM])
-        searchViewModel = JamiSearchViewModel(with: injectionBag, source: dataSource)
+        searchViewModel = JamiSearchViewModel(with: injectionBag, source: dataSource, searchOnlyExistingConversations: false)
     }
 
     override func tearDownWithError() throws {