From dffbe77ff25fa6c90afa0198c679c9980d6c69c5 Mon Sep 17 00:00:00 2001
From: Bruno Muniz Azevedo Filho <bruno@elixxir.io>
Date: Tue, 6 Dec 2022 19:01:17 -0300
Subject: [PATCH] Fixes chat 'more' and 'retry message' flows

---
 Package.swift                                 | 20 +++++
 Sources/AppFeature/Dependencies.swift         |  8 ++
 Sources/AppNavigation/PresentChatMore.swift   | 78 +++++++++++++++++++
 Sources/AppNavigation/PresentContact.swift    |  2 +-
 .../AppNavigation/PresentRetryMessage.swift   | 78 +++++++++++++++++++
 .../Controllers/RetrySheetController.swift    | 62 ---------------
 .../Controllers/SheetController.swift         | 52 -------------
 .../Controllers/SingleChatController.swift    | 43 +++++-----
 .../ChatFeature/Views/RetrySheetView.swift    | 51 ------------
 Sources/ChatFeature/Views/SheetButton.swift   | 56 -------------
 Sources/ChatFeature/Views/SheetView.swift     | 44 -----------
 Sources/ChatMoreFeature/ChatMoreButton.swift  | 31 ++++++++
 .../ChatMoreFeature/ChatMoreController.swift  | 50 ++++++++++++
 Sources/ChatMoreFeature/ChatMoreView.swift    | 44 +++++++++++
 .../RetryMessageButton.swift                  | 31 ++++++++
 .../RetryMessageController.swift              | 50 ++++++++++++
 .../RetryMessageView.swift                    | 42 ++++++++++
 17 files changed, 457 insertions(+), 285 deletions(-)
 create mode 100644 Sources/AppNavigation/PresentChatMore.swift
 create mode 100644 Sources/AppNavigation/PresentRetryMessage.swift
 delete mode 100644 Sources/ChatFeature/Controllers/RetrySheetController.swift
 delete mode 100644 Sources/ChatFeature/Controllers/SheetController.swift
 delete mode 100644 Sources/ChatFeature/Views/RetrySheetView.swift
 delete mode 100644 Sources/ChatFeature/Views/SheetButton.swift
 delete mode 100644 Sources/ChatFeature/Views/SheetView.swift
 create mode 100644 Sources/ChatMoreFeature/ChatMoreButton.swift
 create mode 100644 Sources/ChatMoreFeature/ChatMoreController.swift
 create mode 100644 Sources/ChatMoreFeature/ChatMoreView.swift
 create mode 100644 Sources/RetryMessageFeature/RetryMessageButton.swift
 create mode 100644 Sources/RetryMessageFeature/RetryMessageController.swift
 create mode 100644 Sources/RetryMessageFeature/RetryMessageView.swift

diff --git a/Package.swift b/Package.swift
index ef4dc8f0..ab38fe88 100644
--- a/Package.swift
+++ b/Package.swift
@@ -34,6 +34,7 @@ let package = Package(
     .library(name: "ContactFeature", targets: ["ContactFeature"]),
     .library(name: "FetchBannedList", targets: ["FetchBannedList"]),
     .library(name: "SettingsFeature", targets: ["SettingsFeature"]),
+    .library(name: "ChatMoreFeature", targets: ["ChatMoreFeature"]),
     .library(name: "ChatListFeature", targets: ["ChatListFeature"]),
     .library(name: "RequestsFeature", targets: ["RequestsFeature"]),
     .library(name: "ReportingFeature", targets: ["ReportingFeature"]),
@@ -45,6 +46,7 @@ let package = Package(
     .library(name: "CountryListFeature", targets: ["CountryListFeature"]),
     .library(name: "PermissionsFeature", targets: ["PermissionsFeature"]),
     .library(name: "ContactListFeature", targets: ["ContactListFeature"]),
+    .library(name: "RetryMessageFeature", targets: ["RetryMessageFeature"]),
     .library(name: "RequestPermissionFeature", targets: ["RequestPermissionFeature"]),
   ],
   dependencies: [
@@ -135,6 +137,7 @@ let package = Package(
         .target(name: "WebsiteFeature"),
         .target(name: "RestoreFeature"),
         .target(name: "ProfileFeature"),
+        .target(name: "ChatMoreFeature"),
         .target(name: "ChatListFeature"),
         .target(name: "SettingsFeature"),
         .target(name: "RequestsFeature"),
@@ -143,6 +146,7 @@ let package = Package(
         .target(name: "OnboardingFeature"),
         .target(name: "CreateGroupFeature"),
         .target(name: "ContactListFeature"),
+        .target(name: "RetryMessageFeature"),
         .target(name: "RequestPermissionFeature"),
         .product(name: "PulseUI", package: "Pulse"), // TO REMOVE
         .product(name: "PulseLogHandler", package: "Pulse"), // TO REMOVE
@@ -287,6 +291,20 @@ let package = Package(
         ),
       ]
     ),
+    .target(
+      name: "ChatMoreFeature",
+      dependencies: [
+        .target(name: "Shared"),
+        .target(name: "AppResources"),
+      ]
+    ),
+    .target(
+      name: "RetryMessageFeature",
+      dependencies: [
+        .target(name: "Shared"),
+        .target(name: "AppResources"),
+      ]
+    ),
     .target(
       name: "CountryListFeature",
       dependencies: [
@@ -358,8 +376,10 @@ let package = Package(
         .target(name: "Keychain"),
         .target(name: "Voxophone"),
         .target(name: "DrawerFeature"),
+        .target(name: "ChatMoreFeature"),
         .target(name: "ChatInputFeature"),
         .target(name: "ReportingFeature"),
+        .target(name: "RetryMessageFeature"),
         .target(name: "RequestPermissionFeature"),
         .product(name: "ChatLayout", package: "ChatLayout"),
         .product(name: "DifferenceKit", package: "DifferenceKit"),
diff --git a/Sources/AppFeature/Dependencies.swift b/Sources/AppFeature/Dependencies.swift
index 16f2bf78..1b95b798 100644
--- a/Sources/AppFeature/Dependencies.swift
+++ b/Sources/AppFeature/Dependencies.swift
@@ -14,11 +14,13 @@ import ProfileFeature
 import ChatListFeature
 import SettingsFeature
 import RequestsFeature
+import ChatMoreFeature
 import GroupDraftFeature
 import OnboardingFeature
 import CountryListFeature
 import CreateGroupFeature
 import ContactListFeature
+import RetryMessageFeature
 import RequestPermissionFeature
 
 extension NavigatorKey: DependencyKey {
@@ -35,6 +37,12 @@ extension NavigatorKey: DependencyKey {
     PresentPhotoLibraryNavigator(),
     PresentActivitySheetNavigator(),
 
+    PresentChatMoreNavigator(
+      ChatMoreController.init(_:_:_:)
+    ),
+    PresentRetryMessageNavigator(
+      RetryMessageController.init(_:_:_:)
+    ),
     PresentWebsiteNavigator(
       WebsiteController.init(_:)
     ),
diff --git a/Sources/AppNavigation/PresentChatMore.swift b/Sources/AppNavigation/PresentChatMore.swift
new file mode 100644
index 00000000..1762a739
--- /dev/null
+++ b/Sources/AppNavigation/PresentChatMore.swift
@@ -0,0 +1,78 @@
+import UIKit
+
+/// Opens up `ChatMore` on a given parent view controller
+public struct PresentChatMore: Action {
+  /// - Parameters:
+  ///   - didTapClear: Closure that will get called once the user taps on `clear`
+  ///   - didTapReport: Closure that will get called once the user taps on `report`
+  ///   - didTapDetails: Closure that will get called once the user taps on `details`
+  ///   - parent: Parent view controller from which presentation should happen
+  ///   - animated: Animate the transition
+  public init(
+    didTapClear: @escaping () -> Void,
+    didTapReport: @escaping () -> Void,
+    didTapDetails: @escaping () -> Void,
+    from parent: UIViewController,
+    animated: Bool = true
+  ) {
+    self.didTapClear = didTapClear
+    self.didTapReport = didTapReport
+    self.didTapDetails = didTapDetails
+    self.parent = parent
+    self.animated = animated
+  }
+
+  /// Closure that will get called once the user taps on `clear`
+  public var didTapClear: () -> Void
+
+  /// Closure that will get called once the user taps on `report`
+  public var didTapReport: () -> Void
+
+  /// Closure that will get called once the user taps on `details`
+  public var didTapDetails: () -> Void
+
+  /// Parent view controller from which presentation should happen
+  public var parent: UIViewController
+
+  /// Animate the transition
+  public var animated: Bool
+}
+
+/// Performs `PresentChatMore` action
+public struct PresentChatMoreNavigator: TypedNavigator {
+  /// Custom transitioning delegate
+  let transitioningDelegate = BottomTransitioningDelegate()
+
+  /// View controller which should be opened up
+  var viewController: (
+    @escaping () -> Void,
+    @escaping () -> Void,
+    @escaping () -> Void
+  ) -> UIViewController
+
+  /// - Parameters:
+  ///   - viewController: view controller which should be presented
+  public init(_ viewController: @escaping (
+    @escaping () -> Void,
+    @escaping () -> Void,
+    @escaping () -> Void
+  ) -> UIViewController) {
+    self.viewController = viewController
+  }
+
+  public func perform(_ action: PresentChatMore, completion: @escaping () -> Void) {
+    let controller = viewController(
+      action.didTapClear,
+      action.didTapReport,
+      action.didTapDetails
+    )
+    controller.transitioningDelegate = transitioningDelegate
+    controller.modalPresentationStyle = .overFullScreen
+
+    action.parent.present(
+      controller,
+      animated: action.animated,
+      completion: completion
+    )
+  }
+}
diff --git a/Sources/AppNavigation/PresentContact.swift b/Sources/AppNavigation/PresentContact.swift
index dd1fca4b..0e23eb7d 100644
--- a/Sources/AppNavigation/PresentContact.swift
+++ b/Sources/AppNavigation/PresentContact.swift
@@ -17,7 +17,7 @@ public struct PresentContact: Action {
     self.animated = animated
   }
 
-  /// Model to build the view controller which will be pushed
+  /// Model to build the view controller which will be opened up
   public var contact: Contact
 
   /// Navigation controller on which push should happen
diff --git a/Sources/AppNavigation/PresentRetryMessage.swift b/Sources/AppNavigation/PresentRetryMessage.swift
new file mode 100644
index 00000000..60d65129
--- /dev/null
+++ b/Sources/AppNavigation/PresentRetryMessage.swift
@@ -0,0 +1,78 @@
+import UIKit
+
+/// Opens up `RetryMessage` on a given parent view controller
+public struct PresentRetryMessage: Action {
+  /// - Parameters:
+  ///   - didTapRetry: Closure that will get called once the user taps on `retry`
+  ///   - didTapDelete: Closure that will get called once the user taps on `delete`
+  ///   - didTapCancel: Closure that will get called once the user taps on `cancel`
+  ///   - parent: Parent view controller from which presentation should happen
+  ///   - animated: Animate the transition
+  public init(
+    didTapRetry: @escaping () -> Void,
+    didTapDelete: @escaping () -> Void,
+    didTapCancel: @escaping () -> Void,
+    from parent: UIViewController,
+    animated: Bool = true
+  ) {
+    self.didTapRetry = didTapRetry
+    self.didTapDelete = didTapDelete
+    self.didTapCancel = didTapCancel
+    self.parent = parent
+    self.animated = animated
+  }
+
+  /// Closure that will get called once the user taps on `retry`
+  public var didTapRetry: () -> Void
+
+  /// Closure that will get called once the user taps on `delete`
+  public var didTapDelete: () -> Void
+
+  /// Closure that will get called once the user taps on `cancel`
+  public var didTapCancel: () -> Void
+
+  /// Parent view controller from which presentation should happen
+  public var parent: UIViewController
+
+  /// Animate the transition
+  public var animated: Bool
+}
+
+/// Performs `PresentRetryMessage` action
+public struct PresentRetryMessageNavigator: TypedNavigator {
+  /// Custom transitioning delegate
+  let transitioningDelegate = BottomTransitioningDelegate()
+
+  /// View controller which should be opened up
+  var viewController: (
+    @escaping () -> Void,
+    @escaping () -> Void,
+    @escaping () -> Void
+  ) -> UIViewController
+
+  /// - Parameters:
+  ///   - viewController: view controller which should be presented
+  public init(_ viewController: @escaping (
+    @escaping () -> Void,
+    @escaping () -> Void,
+    @escaping () -> Void
+  ) -> UIViewController) {
+    self.viewController = viewController
+  }
+
+  public func perform(_ action: PresentRetryMessage, completion: @escaping () -> Void) {
+    let controller = viewController(
+      action.didTapRetry,
+      action.didTapDelete,
+      action.didTapCancel
+    )
+    controller.transitioningDelegate = transitioningDelegate
+    controller.modalPresentationStyle = .overFullScreen
+
+    action.parent.present(
+      controller,
+      animated: action.animated,
+      completion: completion
+    )
+  }
+}
diff --git a/Sources/ChatFeature/Controllers/RetrySheetController.swift b/Sources/ChatFeature/Controllers/RetrySheetController.swift
deleted file mode 100644
index 4605017e..00000000
--- a/Sources/ChatFeature/Controllers/RetrySheetController.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-import UIKit
-import Combine
-
-public final class RetrySheetController: UIViewController {
-    enum Action {
-        case retry
-        case delete
-        case cancel
-    }
-
-    // MARK: UI
-
-    private lazy var screenView = RetrySheetView()
-
-    // MARK: Properties
-
-    var actionPublisher: AnyPublisher<Action, Never> {
-        actionRelay.eraseToAnyPublisher()
-    }
-
-    private var cancellables = Set<AnyCancellable>()
-    private let actionRelay = PassthroughSubject<Action, Never>()
-
-    // MARK: Lifecycle
-
-    public override func loadView() {
-        view = screenView
-    }
-
-    public override func viewDidLoad() {
-        super.viewDidLoad()
-        setupBindings()
-    }
-
-    // MARK: Private
-
-    private func setupBindings() {
-        screenView.retry
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] in
-                dismiss(animated: true) { [weak actionRelay] in
-                    actionRelay?.send(.retry)
-                }
-            }.store(in: &cancellables)
-
-        screenView.delete
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] in
-                dismiss(animated: true) { [weak actionRelay] in
-                    actionRelay?.send(.delete)
-                }
-            }.store(in: &cancellables)
-
-        screenView.cancel
-            .publisher(for: .touchUpInside)
-            .sink { [unowned self] in
-                dismiss(animated: true) { [weak actionRelay] in
-                    actionRelay?.send(.cancel)
-                }
-            }.store(in: &cancellables)
-    }
-}
diff --git a/Sources/ChatFeature/Controllers/SheetController.swift b/Sources/ChatFeature/Controllers/SheetController.swift
deleted file mode 100644
index 12dd2e5a..00000000
--- a/Sources/ChatFeature/Controllers/SheetController.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-import UIKit
-import Combine
-
-final class SheetController: UIViewController {
-  enum Action {
-    case clear
-    case details
-    case report
-  }
-
-  private lazy var screenView = SheetView()
-
-  var actionPublisher: AnyPublisher<Action, Never> {
-    actionRelay.eraseToAnyPublisher()
-  }
-
-  private var cancellables = Set<AnyCancellable>()
-  private let actionRelay = PassthroughSubject<Action, Never>()
-
-  public override func loadView() {
-    view = screenView
-  }
-
-  public override func viewDidLoad() {
-    super.viewDidLoad()
-
-    screenView
-      .clearButton
-      .publisher(for: .touchUpInside)
-      .sink { [unowned self] in
-        dismiss(animated: true) { [weak actionRelay] in
-          actionRelay?.send(.clear)
-        }
-      }.store(in: &cancellables)
-
-    screenView.detailsButton
-      .publisher(for: .touchUpInside)
-      .sink { [unowned self] in
-        dismiss(animated: true) { [weak actionRelay] in
-          actionRelay?.send(.details)
-        }
-      }.store(in: &cancellables)
-
-    screenView.reportButton
-      .publisher(for: .touchUpInside)
-      .sink { [unowned self] in
-        dismiss(animated: true) { [weak actionRelay] in
-          actionRelay?.send(.report)
-        }
-      }.store(in: &cancellables)
-  }
-}
diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift
index 5e69a83f..71a5a1c0 100644
--- a/Sources/ChatFeature/Controllers/SingleChatController.swift
+++ b/Sources/ChatFeature/Controllers/SingleChatController.swift
@@ -40,7 +40,6 @@ public final class SingleChatController: UIViewController {
 
   private lazy var moreButton = UIButton()
   private lazy var screenView = ChatView()
-  private lazy var sheet = SheetController()
 
   private let inputComponent: ChatInputView
   private var collectionView: UICollectionView!
@@ -264,23 +263,6 @@ public final class SingleChatController: UIViewController {
   }
 
   private func setupBindings() {
-    sheet
-      .actionPublisher
-      .receive(on: DispatchQueue.main)
-      .sink { [unowned self] in
-        switch $0 {
-        case .clear:
-          presentDeleteAllDrawer()
-        case .details:
-          navigator.perform(PresentContact(
-            contact: viewModel.contact,
-            on: navigationController!
-          ))
-        case .report:
-          presentReportDrawer()
-        }
-      }.store(in: &cancellables)
-
     viewModel
       .shouldDisplayEmptyView
       .removeDuplicates()
@@ -517,7 +499,30 @@ public final class SingleChatController: UIViewController {
   }
 
   @objc private func didTapDots() {
-    //coordinator.toMenuSheet(sheet, from: self)
+    navigator.perform(PresentChatMore(
+      didTapClear: { [weak self] in
+        guard let self else { return }
+        self.navigator.perform(DismissModal(from: self)) {
+          self.presentDeleteAllDrawer()
+        }
+      },
+      didTapReport: { [weak self] in
+        guard let self else { return }
+        self.navigator.perform(DismissModal(from: self)) {
+          self.presentReportDrawer()
+        }
+      },
+      didTapDetails: { [weak self] in
+        guard let self else { return }
+        self.navigator.perform(DismissModal(from: self)) {
+          self.navigator.perform(PresentContact(
+            contact: self.viewModel.contact,
+            on: self.navigationController!
+          ))
+        }
+      },
+      from: self
+    ))
   }
 
   @objc private func didTapInfo() {
diff --git a/Sources/ChatFeature/Views/RetrySheetView.swift b/Sources/ChatFeature/Views/RetrySheetView.swift
deleted file mode 100644
index 1b79665b..00000000
--- a/Sources/ChatFeature/Views/RetrySheetView.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-import UIKit
-import Shared
-import AppResources
-
-final class RetrySheetView: UIView {
-    // MARK: UI
-
-    let stack = UIStackView()
-    let retry = SheetButton()
-    let delete = SheetButton()
-    let cancel = SheetButton(.destructive)
-
-    // MARK: Lifecycle
-
-    init() {
-        super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    // MARK: Private
-
-    private func setup() {
-        layer.cornerRadius = 15
-        layer.masksToBounds = true
-        backgroundColor = Asset.neutralWhite.color
-
-        retry.title.text = Localized.Chat.RetrySheet.retry
-        delete.title.text = Localized.Chat.RetrySheet.delete
-        cancel.title.text = Localized.Chat.RetrySheet.cancel
-
-        retry.image.image = Asset.lens.image
-        delete.image.image = Asset.lens.image
-        cancel.image.image = Asset.lens.image
-
-        stack.axis = .vertical
-        stack.distribution = .fillEqually
-        stack.addArrangedSubview(retry)
-        stack.addArrangedSubview(delete)
-        stack.addArrangedSubview(cancel)
-
-        addSubview(stack)
-
-        stack.snp.makeConstraints { make in
-            make.top.equalToSuperview().offset(10)
-            make.left.right.equalToSuperview()
-            make.bottom.equalTo(safeAreaLayoutGuide)
-        }
-    }
-}
diff --git a/Sources/ChatFeature/Views/SheetButton.swift b/Sources/ChatFeature/Views/SheetButton.swift
deleted file mode 100644
index f24e8a06..00000000
--- a/Sources/ChatFeature/Views/SheetButton.swift
+++ /dev/null
@@ -1,56 +0,0 @@
-import UIKit
-import Shared
-import AppResources
-
-final class SheetButton: UIControl {
-    enum Style {
-        case normal
-        case destructive
-    }
-
-    // MARK: UI
-
-    let title = UILabel()
-    let image = UIImageView()
-
-    // MARK: Properties
-
-    private let style: Style
-    override var isEnabled: Bool {
-        didSet {
-            title.alpha = isEnabled ? 1.0 : 0.5
-            image.alpha = isEnabled ? 1.0 : 0.5
-        }
-    }
-
-    // MARK: Lifecycle
-
-    init(_ style: Style = .normal) {
-        self.style = style
-        super.init(frame: .zero)
-        setup()
-    }
-
-    required init?(coder: NSCoder) { nil }
-
-    // MARK: Private
-
-    private func setup() {
-        title.font = Fonts.Mulish.bold.font(size: 14.0)
-        title.textColor = style == .normal ? Asset.neutralBody.color : Asset.neutralBody.color
-
-        addSubview(title)
-        addSubview(image)
-
-        image.snp.makeConstraints { make in
-            make.left.equalToSuperview().offset(40)
-            make.centerY.equalToSuperview()
-        }
-
-        title.snp.makeConstraints { make in
-            make.left.equalToSuperview().offset(84)
-            make.centerY.equalToSuperview()
-            make.top.equalToSuperview().offset(16)
-        }
-    }
-}
diff --git a/Sources/ChatFeature/Views/SheetView.swift b/Sources/ChatFeature/Views/SheetView.swift
deleted file mode 100644
index 6e305a6f..00000000
--- a/Sources/ChatFeature/Views/SheetView.swift
+++ /dev/null
@@ -1,44 +0,0 @@
-import UIKit
-import Shared
-import AppResources
-
-final class SheetView: UIView {
-    let stackView = UIStackView()
-    let clearButton = SheetButton()
-    let reportButton = SheetButton()
-    let detailsButton = SheetButton()
-
-    init() {
-        super.init(frame: .zero)
-
-        layer.cornerRadius = 40
-        layer.masksToBounds = true
-        backgroundColor = Asset.neutralWhite.color
-
-        clearButton.image.image = Asset.chatListDeleteSwipe.image
-        clearButton.title.text = Localized.Chat.SheetMenu.clear
-
-        detailsButton.tintColor = Asset.neutralDark.color
-        detailsButton.image.image = Asset.searchUsername.image
-        detailsButton.title.text = Localized.Chat.SheetMenu.details
-
-        reportButton.tintColor = Asset.accentDanger.color
-        reportButton.image.image = Asset.searchUsername.image
-        reportButton.title.text = Localized.Chat.SheetMenu.report
-
-        stackView.axis = .vertical
-        stackView.distribution = .fillEqually
-        stackView.addArrangedSubview(clearButton)
-        stackView.addArrangedSubview(detailsButton)
-        stackView.addArrangedSubview(reportButton)
-        addSubview(stackView)
-
-        stackView.snp.makeConstraints {
-            $0.top.equalToSuperview().offset(25)
-            $0.left.right.equalToSuperview()
-            $0.bottom.equalTo(safeAreaLayoutGuide)
-        }
-    }
-
-    required init?(coder: NSCoder) { nil }
-}
diff --git a/Sources/ChatMoreFeature/ChatMoreButton.swift b/Sources/ChatMoreFeature/ChatMoreButton.swift
new file mode 100644
index 00000000..4aced830
--- /dev/null
+++ b/Sources/ChatMoreFeature/ChatMoreButton.swift
@@ -0,0 +1,31 @@
+import UIKit
+import Shared
+import AppResources
+
+final class ChatMoreButton: UIControl {
+  let titleLabel = UILabel()
+  let imageView = UIImageView()
+
+  init() {
+    super.init(frame: .zero)
+
+    titleLabel.font = Fonts.Mulish.bold.font(size: 14.0)
+    titleLabel.textColor = Asset.neutralBody.color
+
+    addSubview(titleLabel)
+    addSubview(imageView)
+
+    imageView.snp.makeConstraints {
+      $0.left.equalToSuperview().offset(40)
+      $0.centerY.equalToSuperview()
+    }
+
+    titleLabel.snp.makeConstraints {
+      $0.left.equalToSuperview().offset(84)
+      $0.centerY.equalToSuperview()
+      $0.top.equalToSuperview().offset(16)
+    }
+  }
+
+  required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/ChatMoreFeature/ChatMoreController.swift b/Sources/ChatMoreFeature/ChatMoreController.swift
new file mode 100644
index 00000000..238358e1
--- /dev/null
+++ b/Sources/ChatMoreFeature/ChatMoreController.swift
@@ -0,0 +1,50 @@
+import UIKit
+import Combine
+
+public final class ChatMoreController: UIViewController {
+  private lazy var screenView = ChatMoreView()
+
+  private let didTapClear: () -> Void
+  private let didTapReport: () -> Void
+  private let didTapDetails: () -> Void
+  private var cancellables = Set<AnyCancellable>()
+
+  public init(
+    _ didTapClear: @escaping () -> Void,
+    _ didTapReport: @escaping () -> Void,
+    _ didTapDetails: @escaping () -> Void
+  ) {
+    self.didTapClear = didTapClear
+    self.didTapReport = didTapReport
+    self.didTapDetails = didTapDetails
+    super.init(nibName: nil, bundle: nil)
+  }
+
+  required init?(coder: NSCoder) { nil }
+
+  public override func loadView() {
+    view = screenView
+  }
+
+  public override func viewDidLoad() {
+    super.viewDidLoad()
+
+    screenView
+      .clearButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapClear() }
+      .store(in: &cancellables)
+
+    screenView
+      .detailsButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapDetails() }
+      .store(in: &cancellables)
+
+    screenView
+      .reportButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapReport() }
+      .store(in: &cancellables)
+  }
+}
diff --git a/Sources/ChatMoreFeature/ChatMoreView.swift b/Sources/ChatMoreFeature/ChatMoreView.swift
new file mode 100644
index 00000000..d994e572
--- /dev/null
+++ b/Sources/ChatMoreFeature/ChatMoreView.swift
@@ -0,0 +1,44 @@
+import UIKit
+import Shared
+import AppResources
+
+final class ChatMoreView: UIView {
+  let clearButton = ChatMoreButton()
+  let reportButton = ChatMoreButton()
+  let detailsButton = ChatMoreButton()
+  private let stackView = UIStackView()
+
+  init() {
+    super.init(frame: .zero)
+
+    layer.cornerRadius = 40
+    layer.masksToBounds = true
+    backgroundColor = Asset.neutralWhite.color
+
+    reportButton.tintColor = Asset.accentDanger.color
+    detailsButton.tintColor = Asset.neutralDark.color
+
+    clearButton.titleLabel.text = Localized.Chat.SheetMenu.clear
+    reportButton.titleLabel.text = Localized.Chat.SheetMenu.report
+    detailsButton.titleLabel.text = Localized.Chat.SheetMenu.details
+
+    reportButton.imageView.image = Asset.searchUsername.image
+    detailsButton.imageView.image = Asset.searchUsername.image
+    clearButton.imageView.image = Asset.chatListDeleteSwipe.image
+
+    stackView.axis = .vertical
+    stackView.distribution = .fillEqually
+    stackView.addArrangedSubview(clearButton)
+    stackView.addArrangedSubview(detailsButton)
+    stackView.addArrangedSubview(reportButton)
+    addSubview(stackView)
+
+    stackView.snp.makeConstraints {
+      $0.top.equalToSuperview().offset(25)
+      $0.left.right.equalToSuperview()
+      $0.bottom.equalTo(safeAreaLayoutGuide)
+    }
+  }
+
+  required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/RetryMessageFeature/RetryMessageButton.swift b/Sources/RetryMessageFeature/RetryMessageButton.swift
new file mode 100644
index 00000000..d4b7722c
--- /dev/null
+++ b/Sources/RetryMessageFeature/RetryMessageButton.swift
@@ -0,0 +1,31 @@
+import UIKit
+import Shared
+import AppResources
+
+final class RetryMessageButton: UIControl {
+  let titleLabel = UILabel()
+  let imageView = UIImageView()
+
+  init() {
+    super.init(frame: .zero)
+
+    titleLabel.textColor = Asset.neutralBody.color
+    titleLabel.font = Fonts.Mulish.bold.font(size: 14.0)
+
+    addSubview(titleLabel)
+    addSubview(imageView)
+
+    imageView.snp.makeConstraints {
+      $0.left.equalToSuperview().offset(40)
+      $0.centerY.equalToSuperview()
+    }
+
+    titleLabel.snp.makeConstraints {
+      $0.left.equalToSuperview().offset(84)
+      $0.centerY.equalToSuperview()
+      $0.top.equalToSuperview().offset(16)
+    }
+  }
+
+  required init?(coder: NSCoder) { nil }
+}
diff --git a/Sources/RetryMessageFeature/RetryMessageController.swift b/Sources/RetryMessageFeature/RetryMessageController.swift
new file mode 100644
index 00000000..3d93cbbc
--- /dev/null
+++ b/Sources/RetryMessageFeature/RetryMessageController.swift
@@ -0,0 +1,50 @@
+import UIKit
+import Combine
+
+public final class RetryMessageController: UIViewController {
+  private lazy var screenView = RetryMessageView()
+
+  private let didTapRetry: () -> Void
+  private let didTapDelete: () -> Void
+  private let didTapCancel: () -> Void
+  private var cancellables = Set<AnyCancellable>()
+
+  public init(
+    _ didTapRetry: @escaping () -> Void,
+    _ didTapDelete: @escaping () -> Void,
+    _ didTapCancel: @escaping () -> Void
+  ) {
+    self.didTapRetry = didTapRetry
+    self.didTapDelete = didTapDelete
+    self.didTapCancel = didTapCancel
+    super.init(nibName: nil, bundle: nil)
+  }
+
+  required init?(coder: NSCoder) { nil }
+
+  public override func loadView() {
+    view = screenView
+  }
+
+  public override func viewDidLoad() {
+    super.viewDidLoad()
+
+    screenView
+      .retryButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapRetry() }
+      .store(in: &cancellables)
+
+    screenView
+      .deleteButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapDelete() }
+      .store(in: &cancellables)
+
+    screenView
+      .cancelButton
+      .publisher(for: .touchUpInside)
+      .sink { [unowned self] in didTapCancel() }
+      .store(in: &cancellables)
+  }
+}
diff --git a/Sources/RetryMessageFeature/RetryMessageView.swift b/Sources/RetryMessageFeature/RetryMessageView.swift
new file mode 100644
index 00000000..a50eb7e9
--- /dev/null
+++ b/Sources/RetryMessageFeature/RetryMessageView.swift
@@ -0,0 +1,42 @@
+import UIKit
+import Shared
+import AppResources
+
+final class RetryMessageView: UIView {
+  private let stackView = UIStackView()
+  let retryButton = RetryMessageButton()
+  let deleteButton = RetryMessageButton()
+  let cancelButton = RetryMessageButton()
+
+  init() {
+    super.init(frame: .zero)
+
+    layer.cornerRadius = 15
+    layer.masksToBounds = true
+    backgroundColor = Asset.neutralWhite.color
+
+    retryButton.titleLabel.text = Localized.Chat.RetrySheet.retry
+    deleteButton.titleLabel.text = Localized.Chat.RetrySheet.delete
+    cancelButton.titleLabel.text = Localized.Chat.RetrySheet.cancel
+
+    retryButton.imageView.image = Asset.lens.image
+    deleteButton.imageView.image = Asset.lens.image
+    cancelButton.imageView.image = Asset.lens.image
+
+    stackView.axis = .vertical
+    stackView.distribution = .fillEqually
+    stackView.addArrangedSubview(retryButton)
+    stackView.addArrangedSubview(deleteButton)
+    stackView.addArrangedSubview(cancelButton)
+
+    addSubview(stackView)
+
+    stackView.snp.makeConstraints {
+      $0.top.equalToSuperview().offset(10)
+      $0.left.right.equalToSuperview()
+      $0.bottom.equalTo(safeAreaLayoutGuide)
+    }
+  }
+
+  required init?(coder: NSCoder) { nil }
+}
-- 
GitLab