// // NotificationController.swift // PrivadoVPN // // Created by Juraldinio on 3/28/21. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation protocol NotificationControllerInput: NSViewController { func updateView() func refreshView(exists: Bool) } protocol NotificationControllerOutput: AnyObject { func notificationListReady(_ view: NotificationControllerInput) func notificationLostClose(_ view: NotificationControllerInput) // func notificationEmptyMessage(_ view: NotificationControllerInput) -> String // func notificationRecordsCount(in view: NotificationControllerInput) -> Int func notificationRecord(in view: NotificationControllerInput, at index: Int) -> NotificationCellModel? // func viewDidAppear() } final class NotificationController: NSViewController { typealias HeaderCreationClosure = () -> NSViewController // swiftlint:disable nesting private enum Constants { static let columnIdentifier = NSUserInterfaceItemIdentifier("NotificationList") static let cellIdentifier = NSUserInterfaceItemIdentifier("NotificationCell") enum Geometry { enum Close { static let width: CGFloat = 23 static let height: CGFloat = 23 static let top: CGFloat = 11 static let trailing: CGFloat = 10 } static let closeBetweenList: CGFloat = 3 } enum Color { static let background = NSColor(red: 45, green: 51, blue: 82) static let emptyText = NSColor(red: 122, green: 134, blue: 190) } enum Font { // swiftlint:disable nesting enum Name { static let emptyText = PrivadoConstants.Font.light } enum Size { static let emptyText: CGFloat = 14 } // swiftlint:enable nesting } static let imageCloseButton = "close" } // swiftlint:enable nesting private let output: NotificationControllerOutput private var header: NSViewController? private var headerCreationClosure: HeaderCreationClosure private let buttonClose: ImageView private let scrollView: NSScrollView private let notificationsView: NSTableView private let emptyTextField: NSTextField // MARK: - JSNavigationBarViewControllerProvider var navigationController: JSNavigationController? // MARK: - Init init(output: NotificationControllerOutput, headerCreation: @escaping HeaderCreationClosure) { self.output = output self.headerCreationClosure = headerCreation self.buttonClose = ImageView(frame: .zero) self.notificationsView = NSTableView(frame: .zero) self.scrollView = NSScrollView(frame: .zero) self.emptyTextField = NSTextField(frame: .zero) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Lyfecycle override func loadView() { let view = View(frame: .zero) view.autoresizesSubviews = true view.translatesAutoresizingMaskIntoConstraints = false view.wantsLayer = true view.viewBackgroundColor = Constants.Color.background self.view = view } override func viewDidLoad() { super.viewDidLoad() self.configureUI() self.layoutUI() self.output.notificationListReady(self) } override func viewDidAppear() { super.viewDidAppear() self.output.viewDidAppear() if let superview = self.view.superview { NSLayoutConstraint.activate([ self.view.topAnchor.constraint(equalTo: superview.topAnchor), self.view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), self.view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), self.view.bottomAnchor.constraint(equalTo: superview.bottomAnchor) ]) } } // MARK: - UI private func configureUI() { if let image = NSImage(named: Constants.imageCloseButton) { self.buttonClose.image = image self.buttonClose.useHandCursor = true self.buttonClose.target = self self.buttonClose.action = #selector(self.onCloseAction) self.view.addSubview(self.buttonClose) } let column = NSTableColumn(identifier: Constants.columnIdentifier) column.isEditable = false column.width = 1 self.notificationsView.addTableColumn(column) // self.outlineView.translatesAutoresizingMaskIntoConstraints = false self.notificationsView.backgroundColor = .clear self.notificationsView.selectionHighlightStyle = .none self.notificationsView.rowSizeStyle = .custom self.notificationsView.headerView = nil self.notificationsView.delegate = self self.notificationsView.dataSource = self self.scrollView.isHidden = true self.scrollView.drawsBackground = false self.scrollView.translatesAutoresizingMaskIntoConstraints = false self.scrollView.hasVerticalScroller = true self.scrollView.documentView = self.notificationsView self.view.addSubview(self.scrollView) self.emptyTextField.isHidden = true self.emptyTextField.translatesAutoresizingMaskIntoConstraints = false self.emptyTextField.isBezeled = false self.emptyTextField.drawsBackground = false self.emptyTextField.isEditable = false self.emptyTextField.isSelectable = false self.emptyTextField.focusRingType = .none self.emptyTextField.textColor = Constants.Color.emptyText self.emptyTextField.font = NSFont(name: Constants.Font.Name.emptyText, size: Constants.Font.Size.emptyText) self.emptyTextField.alignment = .center self.emptyTextField.maximumNumberOfLines = 2 self.view.addSubview(self.emptyTextField) // self.view.addSubview(self.outlineView) } private func layoutUI() { var constraints = [NSLayoutConstraint]() if self.buttonClose.superview.isExist { constraints.append(contentsOf: [ self.buttonClose.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Close.top), self.buttonClose.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Close.trailing), self.buttonClose.widthAnchor.constraint(equalToConstant: Constants.Geometry.Close.width), self.buttonClose.heightAnchor.constraint(equalToConstant: Constants.Geometry.Close.height) ]) // constraints.append(self.scrollView.topAnchor.constraint(equalTo: self.buttonClose.bottomAnchor, constant: Constants.Geometry.closeBetweenList)) } else { constraints.append(self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor)) } constraints.append(contentsOf: [ self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) ]) constraints.append(contentsOf: [ self.emptyTextField.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), self.emptyTextField.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), self.emptyTextField.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.5), self.emptyTextField.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.2) ]) NSLayoutConstraint.activate(constraints) } // MARK: - Text height func calculateHeightFor(string: NSAttributedString) -> CGFloat { let textStorage = NSTextStorage(attributedString: string) let textContainer = NSTextContainer(size: NSSize(width: PrivadoConstants.Window.sizeWidth, height: CGFloat.infinity)) let layotmanager = NSLayoutManager() layotmanager.addTextContainer(textContainer) textStorage.addLayoutManager(layotmanager) textContainer.lineFragmentPadding = 0 textContainer.lineBreakMode = .byWordWrapping textStorage.font = NSFont(name: PrivadoConstants.Font.semiBold, size: 15) layotmanager.ensureLayout(for: textContainer) layotmanager.glyphRange(for: textContainer) return layotmanager.usedRect(for: textContainer).height } // MARK: - On Handlers @objc private func onCloseAction() { self.output.notificationLostClose(self) } } // MARK: - NSTabViewDelegate extension NotificationController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard tableView.tableColumns[safe: 0] == tableColumn , tableColumn.isExist else { return nil } guard let model = self.output.notificationRecord(in: self, at: row) else { return nil } tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil) let cell = NotificationRecordCell(frame: .zero) cell.update(with: model) return cell } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { guard let model = self.output.notificationRecord(in: self, at: row) else { return 150 } let size = self.calculateHeightFor(string: model.markdown) return size + 70 } } // MARK: - NSTableViewDataSource extension NotificationController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { self.output.notificationRecordsCount(in: self) } } // MARK: - NotificationControllerInput extension NotificationController: NotificationControllerInput { func updateView() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.output.notificationListReady(self) } } func refreshView(exists: Bool) { if exists { self.notificationsView.reloadData() self.emptyTextField.isHidden = true self.scrollView.isHidden = false } else { self.emptyTextField.stringValue = self.output.notificationEmptyMessage(self) self.emptyTextField.isHidden = false self.scrollView.isHidden = true } } } // MARK: - JSNavigationBarViewControllerProvider extension NotificationController: JSNavigationBarViewControllerProvider { func navigationBarViewController() -> NSViewController { if let header = self.header { return header } let header = self.headerCreationClosure() self.header = header return header } }