import HUD import UIKit import Theme import Shared import Models import Combine import XXModels import DrawerFeature import DependencyInjection import ScrollViewController public final class ContactController: UIViewController { @Dependency private var hud: HUDType @Dependency private var coordinator: ContactCoordinating @Dependency private var statusBarController: StatusBarStyleControlling lazy private var screenView = ContactView() lazy private var scrollViewController = ScrollViewController() private let viewModel: ContactViewModel private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() public init(_ model: Contact) { self.viewModel = ContactViewModel(model) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { nil } public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) statusBarController.style.send(.lightContent) navigationController?.navigationBar .customize(backgroundColor: Asset.neutralBody.color) } public override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() screenView.updateTopOffset(-view.safeAreaInsets.top) screenView.updateBottomOffset(view.safeAreaInsets.bottom) } public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupScrollView() setupBindings() screenView.didTapSend = { [weak self] in guard let self = self else { return } self.coordinator.toSingleChat(with: self.viewModel.contact, from: self) } screenView.didTapInfo = { [weak self] in self?.presentInfo( title: Localized.Contact.SendMessage.Info.title, subtitle: Localized.Contact.SendMessage.Info.subtitle, urlString: "https://links.xx.network/cmix" ) } screenView.set(status: viewModel.contact.authStatus) } private func setupNavigationBar() { navigationItem.backButtonTitle = "" let back = UIButton.back(color: Asset.neutralWhite.color) back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: back) } private func setupScrollView() { addChild(scrollViewController) view.addSubview(scrollViewController.view) scrollViewController.view.backgroundColor = Asset.neutralWhite.color scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } scrollViewController.didMove(toParent: self) scrollViewController.contentView = screenView scrollViewController.scrollView.bounces = false } private func setupBindings() { viewModel.hudPublisher .receive(on: DispatchQueue.main) .sink { [hud] in hud.update(with: $0) } .store(in: &cancellables) screenView.cardComponent.avatarView.editButton .publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { [unowned self] in coordinator.toPhotos(from: self) } .store(in: &cancellables) viewModel.statePublisher .map(\.photo) .compactMap { $0 } .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.cardComponent.image = $0 } .store(in: &cancellables) viewModel.statePublisher .map(\.title) .compactMap { $0 } .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.cardComponent.nameLabel.text = $0 } .store(in: &cancellables) viewModel.popPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in navigationController?.popViewController(animated: true) } .store(in: &cancellables) viewModel.popToRootPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in navigationController?.popToRootViewController(animated: true) } .store(in: &cancellables) viewModel.successPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in screenView.updateToSuccess() } .store(in: &cancellables) setupScannedBindings() setupReceivedBindings() setupConfirmedBindings() setupInProgressBindings() setupSuccessBindings() } private func setupSuccessBindings() { screenView.successView.keepAdding .publisher(for: .touchUpInside) .sink { [unowned self] in navigationController?.popViewController(animated: true) } .store(in: &cancellables) screenView.successView.sentRequests .publisher(for: .touchUpInside) .sink { [unowned self] in coordinator.toRequests(from: self) } .store(in: &cancellables) viewModel.statePublisher .map(\.username) .removeDuplicates() .combineLatest( viewModel.statePublisher.map(\.email).removeDuplicates(), viewModel.statePublisher.map(\.phone).removeDuplicates() ) .sink { [unowned self] in [Localized.Contact.username: $0.0, Localized.Contact.email: $0.1, Localized.Contact.phone: $0.2].forEach { pair in guard let value = pair.value else { return } let attributeView = AttributeComponent() attributeView.set( title: pair.key, value: value ) screenView.successView.stack.addArrangedSubview(attributeView) } }.store(in: &cancellables) } private func setupScannedBindings() { screenView.scannedView.add .publisher(for: .touchUpInside) .sink { [unowned self] in coordinator.toNickname( from: self, prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", viewModel.didTapRequest(with:) ) }.store(in: &cancellables) } private func setupReceivedBindings() { screenView.receivedView.accept .publisher(for: .touchUpInside) .sink { [unowned self] in coordinator.toNickname( from: self, prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", viewModel.didTapAccept(_:) ) }.store(in: &cancellables) screenView.receivedView.reject .publisher(for: .touchUpInside) .sink { [weak viewModel] in viewModel?.didTapReject() } .store(in: &cancellables) } private func setupInProgressBindings() { viewModel.statePublisher .map(\.username) .removeDuplicates() .combineLatest( viewModel.statePublisher.map(\.email).removeDuplicates(), viewModel.statePublisher.map(\.phone).removeDuplicates() ) .sink { [unowned self] in [Localized.Contact.username: $0.0, Localized.Contact.email: $0.1, Localized.Contact.phone: $0.2].forEach { pair in guard let value = pair.value else { return } let attributeView = AttributeComponent() attributeView.set( title: pair.key, value: value ) screenView.inProgressView.stack.addArrangedSubview(attributeView) } }.store(in: &cancellables) screenView.inProgressView.feedback .button.publisher(for: .touchUpInside) .sink { [weak viewModel] in viewModel?.didTapResend() } .store(in: &cancellables) } private func setupConfirmedBindings() { viewModel.statePublisher .receive(on: DispatchQueue.main) .map(\.nickname) .removeDuplicates() .combineLatest( viewModel.statePublisher.map(\.username).removeDuplicates(), viewModel.statePublisher.map(\.email).removeDuplicates(), viewModel.statePublisher.map(\.phone).removeDuplicates() ) .sink { [unowned self] in screenView.confirmedView.stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } let nicknameAttribute = AttributeComponent() nicknameAttribute.set(title: Localized.Contact.nickname, value: $0.0, style: .requiredEditable) screenView.confirmedView.stackView.insertArrangedSubview(nicknameAttribute, at: 0) nicknameAttribute.actionButton.publisher(for: .touchUpInside) .sink { [unowned self] in coordinator.toNickname( from: self, prefilled: (viewModel.contact.nickname ?? viewModel.contact.username) ?? "", viewModel.didUpdateNickname(_:) ) } .store(in: &cancellables) let usernameAttribute = AttributeComponent() usernameAttribute.set(title: Localized.Contact.username, value: $0.1) screenView.confirmedView.stackView.addArrangedSubview(usernameAttribute) let emailAttribute = AttributeComponent() emailAttribute.set(title: Localized.Contact.email, value: $0.2) screenView.confirmedView.stackView.addArrangedSubview(emailAttribute) let phoneAttribute = AttributeComponent() phoneAttribute.set(title: Localized.Contact.phone, value: $0.3) screenView.confirmedView.stackView.addArrangedSubview(phoneAttribute) let deleteButton = RowButton() deleteButton.setup( title: "Delete Connection", icon: Asset.settingsDelete.image, style: .delete, separator: false ) screenView.confirmedView.stackView.addArrangedSubview(deleteButton) deleteButton.publisher(for: .touchUpInside) .sink { [unowned self] in presentDeleteInfo() } .store(in: &cancellables) }.store(in: &cancellables) screenView.confirmedView.clearButton .publisher(for: .touchUpInside) .sink { [unowned self] in presentClearDrawer() } .store(in: &cancellables) } private func presentClearDrawer() { let clearButton = CapsuleButton() clearButton.setStyle(.red) clearButton.setTitle(Localized.Contact.Clear.action, for: .normal) let cancelButton = CapsuleButton() cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.Contact.Clear.cancel, for: .normal) let drawer = DrawerController(with: [ DrawerImage( image: Asset.drawerNegative.image ), DrawerText( font: Fonts.Mulish.semiBold.font(size: 18.0), text: Localized.Contact.Clear.title, color: Asset.neutralActive.color ), DrawerText( font: Fonts.Mulish.semiBold.font(size: 14.0), text: Localized.Contact.Clear.subtitle, color: Asset.neutralWeak.color, lineHeightMultiple: 1.35, spacingAfter: 25 ), DrawerStack( spacing: 20.0, views: [clearButton, cancelButton] ) ]) clearButton.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { drawer.dismiss(animated: true) { [weak self] in guard let self = self else { return } self.drawerCancellables.removeAll() self.viewModel.didTapClear() } }.store(in: &drawerCancellables) cancelButton.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { drawer.dismiss(animated: true) { [weak self] in self?.drawerCancellables.removeAll() } }.store(in: &drawerCancellables) coordinator.toDrawer(drawer, from: self) } @objc private func didTapBack() { navigationController?.popViewController(animated: true) } } extension ContactController: UIImagePickerControllerDelegate { public func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] ) { var image: UIImage? if let originalImage = info[.originalImage] as? UIImage { image = originalImage } if let croppedImage = info[.editedImage] as? UIImage { image = croppedImage } guard let image = image else { picker.dismiss(animated: true) return } picker.dismiss(animated: true) viewModel.didChoosePhoto(image) } } extension ContactController: UINavigationControllerDelegate {} extension ContactController { private func presentInfo( title: String, subtitle: String, urlString: String = "" ) { let actionButton = CapsuleButton() actionButton.set( style: .seeThrough, title: Localized.Settings.InfoDrawer.action ) let drawer = DrawerController(with: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, color: Asset.neutralActive.color, alignment: .left, spacingAfter: 19 ), DrawerLinkText( text: subtitle, urlString: urlString, spacingAfter: 37 ), DrawerStack(views: [ actionButton, FlexibleSpace() ]) ]) actionButton.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { drawer.dismiss(animated: true) { [weak self] in guard let self = self else { return } self.drawerCancellables.removeAll() } }.store(in: &drawerCancellables) coordinator.toDrawer(drawer, from: self) } private func presentDeleteInfo() { let actionButton = DrawerCapsuleButton(model: .init( title: "Delete Connection", style: .red )) let drawer = DrawerController(with: [ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: "Delete Connection?", color: Asset.neutralActive.color, alignment: .left, spacingAfter: 19 ), DrawerText( text: "This is a silent deletion, \(viewModel.contact.username) will not know you deleted them. This action will remove all information on your phone about this user, including your communications. You #cannot undo this step, and cannot re-add them unless they delete you as a connection as well.#", spacingAfter: 37 ), actionButton ]) actionButton.action .receive(on: DispatchQueue.main) .sink { drawer.dismiss(animated: true) { [weak self] in guard let self = self else { return } self.drawerCancellables.removeAll() self.viewModel.didTapDelete() } }.store(in: &drawerCancellables) coordinator.toDrawer(drawer, from: self) } }