|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Foundation | 
					
						
							|  |  |  | import Combine | 
					
						
							|  |  |  | import NVActivityIndicatorView | 
					
						
							|  |  |  | import SessionMessagingKit | 
					
						
							|  |  |  | import SessionUIKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { | 
					
						
							|  |  |  |     private let itemsPerSection: Int = (UIDevice.current.isIPad ? 4 : 2) | 
					
						
							|  |  |  |     private var maxWidth: CGFloat | 
					
						
							|  |  |  |     private var rooms: [OpenGroupAPI.Room] = [] { didSet { update() } } | 
					
						
							|  |  |  |     private var heightConstraint: NSLayoutConstraint! | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     var delegate: OpenGroupSuggestionGridDelegate? | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - UI | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private static let cellHeight: CGFloat = 40 | 
					
						
							|  |  |  |     private static let separatorWidth = Values.separatorThickness | 
					
						
							|  |  |  |     private static let numHorizontalCells: CGFloat = (UIDevice.current.isIPad ? 4 : 2) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var layout: LastRowCenteredLayout = { | 
					
						
							|  |  |  |         let result = LastRowCenteredLayout() | 
					
						
							|  |  |  |         result.minimumLineSpacing = Values.mediumSpacing | 
					
						
							|  |  |  |         result.minimumInteritemSpacing = Values.mediumSpacing | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var collectionView: UICollectionView = { | 
					
						
							|  |  |  |         let result = UICollectionView(frame: .zero, collectionViewLayout: layout) | 
					
						
							|  |  |  |         result.themeBackgroundColor = .clear | 
					
						
							|  |  |  |         result.isScrollEnabled = false | 
					
						
							|  |  |  |         result.register(view: Cell.self) | 
					
						
							|  |  |  |         result.dataSource = self | 
					
						
							|  |  |  |         result.delegate = self | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private let spinner: NVActivityIndicatorView = { | 
					
						
							|  |  |  |         let result: NVActivityIndicatorView = NVActivityIndicatorView( | 
					
						
							|  |  |  |             frame: CGRect.zero, | 
					
						
							|  |  |  |             type: .circleStrokeSpin, | 
					
						
							|  |  |  |             color: .black, | 
					
						
							|  |  |  |             padding: nil | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         result.set(.width, to: OpenGroupSuggestionGrid.cellHeight) | 
					
						
							|  |  |  |         result.set(.height, to: OpenGroupSuggestionGrid.cellHeight) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in | 
					
						
							|  |  |  |             guard let textPrimary: UIColor = theme.color(for: .textPrimary) else { return } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             result?.color = textPrimary | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         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.themeTintColor = .danger | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var errorTitleLabel: UILabel = { | 
					
						
							|  |  |  |         let result: UILabel = UILabel() | 
					
						
							|  |  |  |         result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .medium) | 
					
						
							|  |  |  |         result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE".localized() | 
					
						
							|  |  |  |         result.themeTextColor = .textPrimary | 
					
						
							|  |  |  |         result.textAlignment = .center | 
					
						
							|  |  |  |         result.numberOfLines = 0 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private lazy var errorSubtitleLabel: UILabel = { | 
					
						
							|  |  |  |         let result: UILabel = UILabel() | 
					
						
							|  |  |  |         result.font = .systemFont(ofSize: Values.smallFontSize, weight: .medium) | 
					
						
							|  |  |  |         result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE".localized() | 
					
						
							|  |  |  |         result.themeTextColor = .textPrimary | 
					
						
							|  |  |  |         result.textAlignment = .center | 
					
						
							|  |  |  |         result.numberOfLines = 0 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return result | 
					
						
							|  |  |  |     }() | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // 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 | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         OpenGroupManager.getDefaultRoomsIfNeeded() | 
					
						
							|  |  |  |             .receive(on: DispatchQueue.main) | 
					
						
							|  |  |  |             .sinkUntilComplete( | 
					
						
							|  |  |  |                 receiveCompletion: { [weak self] _ in self?.update() }, | 
					
						
							|  |  |  |                 receiveValue: { [weak self] rooms in self?.rooms = rooms } | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Updating | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private func update() { | 
					
						
							|  |  |  |         spinner.stopAnimating() | 
					
						
							|  |  |  |         spinner.isHidden = true | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let roomCount: CGFloat = CGFloat(min(rooms.count, 8)) // Cap to a maximum of 8 (4 rows of 2) | 
					
						
							|  |  |  |         let numRows: CGFloat = ceil(roomCount / OpenGroupSuggestionGrid.numHorizontalCells) | 
					
						
							|  |  |  |         let height: CGFloat = ((OpenGroupSuggestionGrid.cellHeight * numRows) + ((numRows - 1) * layout.minimumLineSpacing)) | 
					
						
							|  |  |  |         heightConstraint.constant = height | 
					
						
							|  |  |  |         collectionView.reloadData() | 
					
						
							|  |  |  |         errorView.isHidden = (roomCount > 0) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     public func refreshLayout(with maxWidth: CGFloat) { | 
					
						
							|  |  |  |         self.maxWidth = maxWidth | 
					
						
							|  |  |  |         collectionView.collectionViewLayout.invalidateLayout() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Layout | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { | 
					
						
							|  |  |  |         guard | 
					
						
							|  |  |  |             indexPath.item == (collectionView.numberOfItems(inSection: indexPath.section) - 1) && | 
					
						
							|  |  |  |             indexPath.item % 2 == 0 | 
					
						
							|  |  |  |         else { | 
					
						
							|  |  |  |             let cellWidth: CGFloat = ((maxWidth / OpenGroupSuggestionGrid.numHorizontalCells) - ((OpenGroupSuggestionGrid.numHorizontalCells - 1) * layout.minimumInteritemSpacing)) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // If the last item is by itself then we want to make it wider | 
					
						
							|  |  |  |         return CGSize( | 
					
						
							|  |  |  |             width: (Cell.calculatedWith(for: rooms[indexPath.item].name)), | 
					
						
							|  |  |  |             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: Cell = collectionView.dequeue(type: Cell.self, for: indexPath) | 
					
						
							|  |  |  |         cell.room = rooms[indexPath.item] | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return cell | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Interaction | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | 
					
						
							|  |  |  |         let room = rooms[indexPath.section * itemsPerSection + indexPath.item] | 
					
						
							|  |  |  |         delegate?.join(room) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MARK: - Cell | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | extension OpenGroupSuggestionGrid { | 
					
						
							|  |  |  |     fileprivate final class Cell: UICollectionViewCell { | 
					
						
							|  |  |  |         private static let labelFont: UIFont = .systemFont(ofSize: Values.smallFontSize) | 
					
						
							|  |  |  |         private static let imageSize: CGFloat = 30 | 
					
						
							|  |  |  |         private static let itemPadding: CGFloat = Values.smallSpacing | 
					
						
							|  |  |  |         private static let contentLeftPadding: CGFloat = 7 | 
					
						
							|  |  |  |         private static let contentRightPadding: CGFloat = Values.veryLargeSpacing | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         fileprivate static func calculatedWith(for title: String) -> CGFloat { | 
					
						
							|  |  |  |             // FIXME: Do the calculations properly in the 'LastRowCenteredLayout' to handle imageless cells | 
					
						
							|  |  |  |             return ( | 
					
						
							|  |  |  |                 contentLeftPadding + | 
					
						
							|  |  |  |                 imageSize + | 
					
						
							|  |  |  |                 itemPadding + | 
					
						
							|  |  |  |                 NSAttributedString(string: title, attributes: [ .font: labelFont ]).size().width + | 
					
						
							|  |  |  |                 contentRightPadding + | 
					
						
							|  |  |  |                 1   // Not sure why this is needed but it seems things are sometimes truncated without it | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         var room: OpenGroupAPI.Room? { didSet { update() } } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         private lazy var snContentView: UIView = { | 
					
						
							|  |  |  |             let result: UIView = UIView() | 
					
						
							|  |  |  |             result.themeBorderColor = .borderSeparator | 
					
						
							|  |  |  |             result.layer.cornerRadius = Cell.contentViewCornerRadius | 
					
						
							|  |  |  |             result.layer.borderWidth = 1 | 
					
						
							|  |  |  |             result.set(.height, to: Cell.contentViewHeight) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return result | 
					
						
							|  |  |  |         }() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         private lazy var imageView: UIImageView = { | 
					
						
							|  |  |  |             let result: UIImageView = UIImageView() | 
					
						
							|  |  |  |             result.set(.width, to: Cell.imageSize) | 
					
						
							|  |  |  |             result.set(.height, to: Cell.imageSize) | 
					
						
							|  |  |  |             result.layer.cornerRadius = (Cell.imageSize / 2) | 
					
						
							|  |  |  |             result.clipsToBounds = true | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return result | 
					
						
							|  |  |  |         }() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         private lazy var label: UILabel = { | 
					
						
							|  |  |  |             let result: UILabel = UILabel() | 
					
						
							|  |  |  |             result.font = Cell.labelFont | 
					
						
							|  |  |  |             result.themeTextColor = .textPrimary | 
					
						
							|  |  |  |             result.lineBreakMode = .byTruncatingTail | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return result | 
					
						
							|  |  |  |         }() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         private static let contentViewInset: CGFloat = 0 | 
					
						
							|  |  |  |         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() { | 
					
						
							|  |  |  |             backgroundView = UIView() | 
					
						
							|  |  |  |             backgroundView?.themeBackgroundColor = .backgroundPrimary | 
					
						
							|  |  |  |             backgroundView?.layer.cornerRadius = Cell.contentViewCornerRadius | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             selectedBackgroundView = UIView() | 
					
						
							|  |  |  |             selectedBackgroundView?.themeBackgroundColor = .backgroundSecondary | 
					
						
							|  |  |  |             selectedBackgroundView?.layer.cornerRadius = Cell.contentViewCornerRadius | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             addSubview(snContentView) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             let stackView = UIStackView(arrangedSubviews: [ imageView, label ]) | 
					
						
							|  |  |  |             stackView.axis = .horizontal | 
					
						
							|  |  |  |             stackView.spacing = Cell.itemPadding | 
					
						
							|  |  |  |             snContentView.addSubview(stackView) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             stackView.center(.vertical, in: snContentView) | 
					
						
							|  |  |  |             stackView.pin(.leading, to: .leading, of: snContentView, withInset: Cell.contentLeftPadding) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             snContentView.trailingAnchor | 
					
						
							|  |  |  |                 .constraint( | 
					
						
							|  |  |  |                     greaterThanOrEqualTo: stackView.trailingAnchor, | 
					
						
							|  |  |  |                     constant: Cell.contentRightPadding | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |                 .isActive = true | 
					
						
							|  |  |  |             snContentView.pin(to: self) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         private func update() { | 
					
						
							|  |  |  |             guard let room: OpenGroupAPI.Room = room else { return } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             label.text = room.name | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             // Only continue if we have a room image | 
					
						
							|  |  |  |             guard let imageId: String = room.imageId else { | 
					
						
							|  |  |  |                 imageView.isHidden = true | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             imageView.image = nil | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             Publishers | 
					
						
							|  |  |  |                 .MergeMany( | 
					
						
							|  |  |  |                     Storage.shared | 
					
						
							|  |  |  |                         .readPublisherFlatMap { db in | 
					
						
							|  |  |  |                             OpenGroupManager | 
					
						
							|  |  |  |                                 .roomImage(db, fileId: imageId, for: room.token, on: OpenGroupAPI.defaultServer) | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                         .map { ($0, true) } | 
					
						
							|  |  |  |                         .eraseToAnyPublisher(), | 
					
						
							|  |  |  |                     // If we have already received the room image then the above will emit first and | 
					
						
							|  |  |  |                     // we can ignore this 'Just' call which is used to hide the image while loading | 
					
						
							|  |  |  |                     Just((Data(), false)) | 
					
						
							|  |  |  |                         .setFailureType(to: Error.self) | 
					
						
							|  |  |  |                         .delay(for: .milliseconds(10), scheduler: DispatchQueue.main) | 
					
						
							|  |  |  |                         .eraseToAnyPublisher() | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |                 .receiveOnMain(immediately: true) | 
					
						
							|  |  |  |                 .sinkUntilComplete( | 
					
						
							|  |  |  |                     receiveValue: { [weak self] imageData, hasData in | 
					
						
							|  |  |  |                         guard hasData else { | 
					
						
							|  |  |  |                             // This will emit twice (once with the data and once without it), if we | 
					
						
							|  |  |  |                             // have actually received the images then we don't want the second emission | 
					
						
							|  |  |  |                             // to hide the imageView anymore | 
					
						
							|  |  |  |                             if self?.imageView.image == nil { | 
					
						
							|  |  |  |                                 self?.imageView.isHidden = true | 
					
						
							|  |  |  |                             } | 
					
						
							|  |  |  |                             return | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                          | 
					
						
							|  |  |  |                         self?.imageView.image = UIImage(data: imageData) | 
					
						
							|  |  |  |                         self?.imageView.isHidden = (self?.imageView.image == nil) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MARK: - Delegate | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | protocol OpenGroupSuggestionGridDelegate { | 
					
						
							|  |  |  |     func join(_ room: OpenGroupAPI.Room) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MARK: - LastRowCenteredLayout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class LastRowCenteredLayout: UICollectionViewFlowLayout { | 
					
						
							|  |  |  |     override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | 
					
						
							|  |  |  |         // If we have an odd number of items then we want to center the last one horizontally | 
					
						
							|  |  |  |         let elementAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElements(in: rect) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // It looks like on "max" devices the rect we are given can be much larger than the size of the | 
					
						
							|  |  |  |         // collection view, as a result we need to try and use the collectionView width here instead | 
					
						
							|  |  |  |         let targetViewWidth: CGFloat = { | 
					
						
							|  |  |  |             guard let collectionView: UICollectionView = self.collectionView, collectionView.frame.width > 0 else { | 
					
						
							|  |  |  |                 return rect.width | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return collectionView.frame.width | 
					
						
							|  |  |  |         }() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         guard | 
					
						
							|  |  |  |             (elementAttributes?.count ?? 0) % 2 == 1, | 
					
						
							|  |  |  |             let lastItemAttributes: UICollectionViewLayoutAttributes = elementAttributes?.last | 
					
						
							|  |  |  |         else { return elementAttributes } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         lastItemAttributes.frame = CGRect( | 
					
						
							|  |  |  |             x: ((targetViewWidth - lastItemAttributes.frame.size.width) / 2), | 
					
						
							|  |  |  |             y: lastItemAttributes.frame.origin.y, | 
					
						
							|  |  |  |             width: lastItemAttributes.frame.size.width, | 
					
						
							|  |  |  |             height: lastItemAttributes.frame.size.height | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return elementAttributes | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |