324 lines
11 KiB
Swift
324 lines
11 KiB
Swift
//
|
|
// 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
|
|
}
|
|
}
|