// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import GRDB import PromiseKit import SessionUIKit import SessionMessagingKit private protocol TableViewTouchDelegate { func tableViewWasTouched(_ tableView: TableView) } private final class TableView: UITableView { var touchDelegate: TableViewTouchDelegate? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { touchDelegate?.tableViewWasTouched(self) return super.hitTest(point, with: event) } } final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate, TableViewTouchDelegate, UITextFieldDelegate, UIScrollViewDelegate { private let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) private var selectedContacts: Set = [] // MARK: - Components private lazy var nameTextField = TextField(placeholder: "vc_create_closed_group_text_field_hint".localized()) private lazy var tableView: TableView = { let result: TableView = TableView() result.dataSource = self result.delegate = self result.touchDelegate = self result.separatorStyle = .none result.backgroundColor = .clear result.isScrollEnabled = false result.register(view: UserCell.self) return result }() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setUpGradientBackground() setUpNavBarStyle() let customTitleFontSize = Values.largeFontSize setNavBarTitle("vc_create_closed_group_title".localized(), customFontSize: customTitleFontSize) // Set up navigation bar buttons let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) closeButton.tintColor = Colors.text navigationItem.leftBarButtonItem = closeButton let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(createClosedGroup)) doneButton.tintColor = Colors.text navigationItem.rightBarButtonItem = doneButton // Set up content setUpViewHierarchy() } private func setUpViewHierarchy() { guard !contactProfiles.isEmpty else { let explanationLabel: UILabel = UILabel() explanationLabel.textColor = Colors.text explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) explanationLabel.numberOfLines = 0 explanationLabel.lineBreakMode = .byWordWrapping explanationLabel.textAlignment = .center explanationLabel.text = NSLocalizedString("vc_create_closed_group_empty_state_message", comment: "") let createNewPrivateChatButton: Button = Button(style: .prominentOutline, size: .large) createNewPrivateChatButton.setTitle(NSLocalizedString("vc_create_closed_group_empty_state_button_title", comment: ""), for: UIControl.State.normal) createNewPrivateChatButton.addTarget(self, action: #selector(createNewDM), for: UIControl.Event.touchUpInside) createNewPrivateChatButton.set(.width, to: 196) let stackView: UIStackView = UIStackView(arrangedSubviews: [ explanationLabel, createNewPrivateChatButton ]) stackView.axis = .vertical stackView.spacing = Values.mediumSpacing stackView.alignment = .center view.addSubview(stackView) stackView.center(.horizontal, in: view) let verticalCenteringConstraint = stackView.center(.vertical, in: view) verticalCenteringConstraint.constant = -16 // Makes things appear centered visually return } let mainStackView: UIStackView = UIStackView() mainStackView.axis = .vertical nameTextField.delegate = self let nameTextFieldContainer: UIView = UIView() nameTextFieldContainer.addSubview(nameTextField) nameTextField.pin(.leading, to: .leading, of: nameTextFieldContainer, withInset: Values.largeSpacing) nameTextField.pin(.top, to: .top, of: nameTextFieldContainer, withInset: Values.mediumSpacing) nameTextFieldContainer.pin(.trailing, to: .trailing, of: nameTextField, withInset: Values.largeSpacing) nameTextFieldContainer.pin(.bottom, to: .bottom, of: nameTextField, withInset: Values.largeSpacing) mainStackView.addArrangedSubview(nameTextFieldContainer) let separator: UIView = UIView() separator.backgroundColor = Colors.separator separator.set(.height, to: Values.separatorThickness) mainStackView.addArrangedSubview(separator) tableView.set(.height, to: CGFloat(contactProfiles.count * 65)) // A cell is exactly 65 points high tableView.set(.width, to: UIScreen.main.bounds.width) mainStackView.addArrangedSubview(tableView) let scrollView: UIScrollView = UIScrollView(wrapping: mainStackView, withInsets: UIEdgeInsets.zero) scrollView.showsVerticalScrollIndicator = false scrollView.delegate = self view.addSubview(scrollView) scrollView.set(.width, to: UIScreen.main.bounds.width) scrollView.pin(to: view) } // MARK: - Table View Data Source func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactProfiles.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath) cell.update( with: contactProfiles[indexPath.row].id, profile: contactProfiles[indexPath.row], isZombie: false, accessory: .tick(isSelected: selectedContacts.contains(contactProfiles[indexPath.row].id)) ) return cell } // MARK: - Interaction func textFieldDidEndEditing(_ textField: UITextField) { crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("vc_create_closed_group_title", comment: "") : textField.text! } fileprivate func tableViewWasTouched(_ tableView: TableView) { if nameTextField.isFirstResponder { nameTextField.resignFirstResponder() } } func scrollViewDidScroll(_ scrollView: UIScrollView) { let nameTextFieldCenterY = nameTextField.convert(nameTextField.bounds.center, to: scrollView).y let tableViewOriginY = tableView.convert(tableView.bounds.origin, to: scrollView).y let titleLabelAlpha = 1 - (scrollView.contentOffset.y - nameTextFieldCenterY) / (tableViewOriginY - nameTextFieldCenterY) let crossfadeLabelAlpha = 1 - titleLabelAlpha navBarTitleLabel.alpha = titleLabelAlpha crossfadeLabel.alpha = crossfadeLabelAlpha } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if !selectedContacts.contains(contactProfiles[indexPath.row].id) { selectedContacts.insert(contactProfiles[indexPath.row].id) } else { selectedContacts.remove(contactProfiles[indexPath.row].id) } tableView.deselectRow(at: indexPath, animated: true) tableView.reloadRows(at: [indexPath], with: .none) } @objc private func close() { dismiss(animated: true, completion: nil) } @objc private func createClosedGroup() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else { return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: "")) } guard name.count < 64 else { return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: "")) } guard selectedContacts.count >= 1 else { return showError(title: "Please pick at least 1 group member") } guard selectedContacts.count < 100 else { // Minus one because we're going to include self later return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: "")) } let selectedContacts = self.selectedContacts let message: String? = (selectedContacts.count > 20) ? "Please wait while the group is created..." : nil ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in Storage.shared .writeAsync { db in try MessageSender.createClosedGroup(db, name: name, members: selectedContacts) } .done(on: DispatchQueue.main) { thread in Storage.shared.writeAsync { db in try? MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() } self?.presentingViewController?.dismiss(animated: true, completion: nil) SessionApp.presentConversation(for: thread.id, action: .compose, animated: false) } .catch(on: DispatchQueue.main) { [weak self] _ in self?.dismiss(animated: true, completion: nil) // Dismiss the loader let title = "Couldn't Create Group" let message = "Please check your internet connection and try again." let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.presentAlert(alert) } .retainUntilComplete() } } @objc private func createNewDM() { presentingViewController?.dismiss(animated: true, completion: nil) SessionApp.homeViewController.wrappedValue?.createNewDM() } }