session-ios/Session/Open Groups/OpenGroupSuggestionGrid.swift
2022-03-11 10:09:21 +11:00

263 lines
9.9 KiB
Swift

import PromiseKit
import NVActivityIndicatorView
import SessionUIKit
final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private let maxWidth: CGFloat
private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { update() } }
private var heightConstraint: NSLayoutConstraint!
var delegate: OpenGroupSuggestionGridDelegate?
// MARK: UI Components
private lazy var layout: UICollectionViewFlowLayout = {
let result = UICollectionViewFlowLayout()
result.minimumLineSpacing = 0
result.minimumInteritemSpacing = 0
return result
}()
private lazy var collectionView: UICollectionView = {
let result = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
result.register(Cell.self, forCellWithReuseIdentifier: Cell.identifier)
result.backgroundColor = .clear
result.isScrollEnabled = false
result.dataSource = self
result.delegate = self
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
result.set(.width, to: OpenGroupSuggestionGrid.cellHeight)
result.set(.height, to: OpenGroupSuggestionGrid.cellHeight)
return result
}()
private lazy var errorView: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var errorImageView: UIImageView = {
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "warning").withRenderingMode(.alwaysTemplate))
result.tintColor = Colors.destructive
return result
}()
private lazy var errorTitleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = UIFont.systemFont(ofSize: Values.mediumFontSize, weight: .medium)
result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE".localized()
result.textColor = Colors.text
result.textAlignment = .center
result.numberOfLines = 0
return result
}()
private lazy var errorSubtitleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = UIFont.systemFont(ofSize: Values.smallFontSize, weight: .medium)
result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE".localized()
result.textColor = Colors.text
result.textAlignment = .center
result.numberOfLines = 0
return result
}()
// MARK: Settings
private static let cellHeight: CGFloat = 40
private static let separatorWidth = 1 / UIScreen.main.scale
// MARK: Initialization
init(maxWidth: CGFloat) {
self.maxWidth = maxWidth
super.init(frame: CGRect.zero)
initialize()
}
override init(frame: CGRect) {
preconditionFailure("Use init(maxWidth:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(maxWidth:) instead.")
}
private func initialize() {
addSubview(collectionView)
collectionView.pin(to: self)
addSubview(spinner)
spinner.pin(.top, to: .top, of: self)
spinner.center(.horizontal, in: self)
spinner.startAnimating()
addSubview(errorView)
errorView.pin(.top, to: .top, of: self, withInset: 10)
errorView.pin( [HorizontalEdge.leading, HorizontalEdge.trailing], to: self)
errorView.addSubview(errorImageView)
errorImageView.pin(.top, to: .top, of: errorView)
errorImageView.center(.horizontal, in: errorView)
errorImageView.set(.width, to: 60)
errorImageView.set(.height, to: 60)
errorView.addSubview(errorTitleLabel)
errorTitleLabel.pin(.top, to: .bottom, of: errorImageView, withInset: 10)
errorTitleLabel.center(.horizontal, in: errorView)
errorView.addSubview(errorSubtitleLabel)
errorSubtitleLabel.pin(.top, to: .bottom, of: errorTitleLabel, withInset: 20)
errorSubtitleLabel.center(.horizontal, in: errorView)
heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight)
widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
.done { [weak self] rooms in
self?.rooms = rooms
}
.catch { [weak self] _ in
self?.update()
}
}
// MARK: Updating
private func update() {
spinner.stopAnimating()
spinner.isHidden = true
let roomCount = min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2)
let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(roomCount) / 2)
heightConstraint.constant = height
collectionView.reloadData()
errorView.isHidden = (roomCount > 0)
}
// MARK: Layout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellWidth = UIDevice.current.isIPad ? maxWidth / 4 : maxWidth / 2
return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight)
}
// MARK: Data Source
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell
cell.room = rooms[indexPath.item]
return cell
}
// MARK: Interaction
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let room = rooms[indexPath.item]
delegate?.join(room)
}
}
// MARK: Cell
extension OpenGroupSuggestionGrid {
fileprivate final class Cell : UICollectionViewCell {
var room: OpenGroupAPIV2.Info? { didSet { update() } }
static let identifier = "OpenGroupSuggestionGridCell"
private lazy var snContentView: UIView = {
let result = UIView()
result.backgroundColor = Colors.navigationBarBackground
result.set(.height, to: Cell.contentViewHeight)
result.layer.cornerRadius = Cell.contentViewCornerRadius
return result
}()
private lazy var imageView: UIImageView = {
let result = UIImageView()
let size: CGFloat = 24
result.set(.width, to: size)
result.set(.height, to: size)
result.layer.cornerRadius = size / 2
result.clipsToBounds = true
return result
}()
private lazy var label: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.smallFontSize)
result.lineBreakMode = .byTruncatingTail
return result
}()
private static let contentViewInset: CGFloat = 4
private static var contentViewHeight: CGFloat { OpenGroupSuggestionGrid.cellHeight - 2 * contentViewInset }
private static var contentViewCornerRadius: CGFloat { contentViewHeight / 2 }
override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
addSubview(snContentView)
let stackView = UIStackView(arrangedSubviews: [ imageView, label ])
stackView.axis = .horizontal
stackView.spacing = Values.smallSpacing
snContentView.addSubview(stackView)
stackView.center(.vertical, in: snContentView)
stackView.pin(.leading, to: .leading, of: snContentView, withInset: 4)
snContentView.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: Values.smallSpacing).isActive = true
snContentView.pin(to: self, withInset: Cell.contentViewInset)
}
override func layoutSubviews() {
super.layoutSubviews()
let newPath = UIBezierPath(roundedRect: snContentView.bounds, cornerRadius: Cell.contentViewCornerRadius).cgPath
snContentView.layer.shadowPath = newPath
snContentView.layer.shadowColor = UIColor.black.cgColor
snContentView.layer.shadowOffset = CGSize.zero
snContentView.layer.shadowOpacity = isLightMode ? 0.2 : 0.6
snContentView.layer.shadowRadius = 2
}
private func update() {
guard let room = room else { return }
let promise = OpenGroupAPIV2.getGroupImage(for: room.id, on: OpenGroupAPIV2.defaultServer)
if let imageData: Data = promise.value {
imageView.image = UIImage(data: imageData)
imageView.isHidden = (imageView.image == nil)
}
else {
imageView.isHidden = true
_ = promise.done { [weak self] imageData in
DispatchQueue.main.async {
self?.imageView.image = UIImage(data: imageData)
self?.imageView.isHidden = (self?.imageView.image == nil)
}
}
}
label.text = room.name
}
}
}
// MARK: Delegate
protocol OpenGroupSuggestionGridDelegate {
func join(_ room: OpenGroupAPIV2.Info)
}