From a7dd7e1bf0175096101bec2b423bb083744a1a4d Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 26 Mar 2021 13:28:40 +1100 Subject: [PATCH] Hook up suggestion grid --- Session/Home/HomeVC.swift | 2 + Session/Meta/AppDelegate.m | 6 +- Session/Open Groups/JoinOpenGroupVC.swift | 46 ++++++++-- .../Open Groups/OpenGroupSuggestionGrid.swift | 84 ++++++++++++++++--- .../Open Groups/V2/OpenGroupAPIV2+ObjC.swift | 5 ++ .../Open Groups/V2/OpenGroupAPIV2.swift | 13 +++ .../Open Groups/V2/OpenGroupManagerV2.swift | 50 ++++++----- 7 files changed, 164 insertions(+), 42 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 238a0f607..13f962a83 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -153,6 +153,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv DispatchQueue.global(qos: .utility).async { let _ = IP2Country.shared.populateCacheIfNeeded() } + // Get default open group rooms if needed + OpenGroupAPIV2.getDefaultRoomsIfNeeded() } override func viewDidAppear(_ animated: Bool) { diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index df72f2c19..4666e3a6c 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -396,7 +396,7 @@ static NSTimeInterval launchStartedAt; [self startClosedGroupPollerIfNeeded]; [self startOpenGroupPollersIfNeeded]; - // Loki: Update profile picture if needed + // Update profile picture if needed NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; NSDate *now = [NSDate new]; NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"]; @@ -410,6 +410,10 @@ static NSTimeInterval launchStartedAt; // Do nothing } requiresSync:YES]; } + + if (CurrentAppContext().isMainApp) { + [SNOpenGroupAPIV2 getDefaultRoomsIfNeeded]; + } if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { OWSLogInfo(@"Retrying remote notification registration since user hasn't registered yet."); diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index f469c76ca..e814b6372 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -149,6 +149,28 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView } } + fileprivate func join(_ room: String, on server: String, with publicKey: String) { + guard !isJoining else { return } + isJoining = true + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in + Storage.shared.write { transaction in + OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction) + .done(on: DispatchQueue.main) { [weak self] _ in + self?.presentingViewController!.dismiss(animated: true, completion: nil) + let appDelegate = UIApplication.shared.delegate as! AppDelegate + appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) + } + .catch(on: DispatchQueue.main) { [weak self] error in + self?.dismiss(animated: true, completion: nil) // Dismiss the loader + let title = "Couldn't Join" + let message = error.localizedDescription + self?.isJoining = false + self?.showError(title: title, message: message) + } + } + } + } + // MARK: Convenience private func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -157,7 +179,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView } } -private final class EnterURLVC : UIViewController { +private final class EnterURLVC : UIViewController, OpenGroupSuggestionGridDelegate { weak var joinOpenGroupVC: JoinOpenGroupVC! // MARK: Components @@ -170,13 +192,22 @@ private final class EnterURLVC : UIViewController { private lazy var suggestionGrid: OpenGroupSuggestionGrid = { let maxWidth = UIScreen.main.bounds.width - Values.largeSpacing * 2 - return OpenGroupSuggestionGrid(maxWidth: maxWidth) + let result = OpenGroupSuggestionGrid(maxWidth: maxWidth) + result.delegate = self + return result }() // MARK: Lifecycle override func viewDidLoad() { // Remove background color view.backgroundColor = .clear + // Suggestion grid title label + let suggestionGridTitleLabel = UILabel() + suggestionGridTitleLabel.textColor = Colors.text + suggestionGridTitleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize) + suggestionGridTitleLabel.text = "Or join one of these..." + suggestionGridTitleLabel.numberOfLines = 0 + suggestionGridTitleLabel.lineBreakMode = .byWordWrapping // Next button let nextButton = Button(style: .prominentOutline, size: .large) nextButton.setTitle(NSLocalizedString("next", comment: ""), for: UIControl.State.normal) @@ -187,11 +218,9 @@ private final class EnterURLVC : UIViewController { nextButton.pin(.top, to: .top, of: nextButtonContainer) nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80) nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton) - // Spacers - let spacer1 = UIView.vStretchingSpacer() - let spacer2 = UIView.vStretchingSpacer() // Stack view - let stackView = UIStackView(arrangedSubviews: [ urlTextField, spacer1, suggestionGrid, spacer2, nextButtonContainer ]) + let stackView = UIStackView(arrangedSubviews: [ urlTextField, UIView.spacer(withHeight: Values.mediumSpacing), suggestionGridTitleLabel, + UIView.spacer(withHeight: Values.mediumSpacing), suggestionGrid, UIView.vStretchingSpacer(), nextButtonContainer ]) stackView.axis = .vertical stackView.alignment = .fill stackView.layoutMargins = UIEdgeInsets(uniform: Values.largeSpacing) @@ -200,7 +229,6 @@ private final class EnterURLVC : UIViewController { stackView.pin(to: view) // Constraints view.set(.width, to: UIScreen.main.bounds.width) - spacer1.heightAnchor.constraint(equalTo: spacer2.heightAnchor).isActive = true // Dismiss keyboard on tap let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) view.addGestureRecognizer(tapGestureRecognizer) @@ -216,6 +244,10 @@ private final class EnterURLVC : UIViewController { } // MARK: Interaction + func join(_ room: OpenGroupAPIV2.Info) { + joinOpenGroupVC.join(room.id, on: OpenGroupAPIV2.defaultServer, with: OpenGroupAPIV2.defaultServerPublicKey) + } + @objc private func joinOpenGroupIfPossible() { let url = urlTextField.text?.trimmingCharacters(in: .whitespaces) ?? "" joinOpenGroupVC.joinOpenGroupIfPossible(with: url) diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index eac7b13f4..787aba2b4 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -1,8 +1,11 @@ +import PromiseKit +import NVActivityIndicatorView final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat - private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { reload() } } + private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { update() } } private var heightConstraint: NSLayoutConstraint! + var delegate: OpenGroupSuggestionGridDelegate? // MARK: UI Components private lazy var layout: UICollectionViewFlowLayout = { @@ -22,6 +25,13 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl return result }() + private lazy var spinner: NVActivityIndicatorView = { + let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil) + result.set(.width, to: 64) + result.set(.height, to: 64) + return result + }() + // MARK: Settings private static let cellHeight: CGFloat = 40 private static let separatorWidth = 1 / UIScreen.main.scale @@ -44,43 +54,59 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl private func initialize() { addSubview(collectionView) collectionView.pin(to: self) - heightConstraint = set(.height, to: 0) - attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - return OpenGroupAPIV2.getAllRooms(from: "https://sessionopengroup.com").done { [weak self] rooms in - self?.rooms = rooms - } - }.retainUntilComplete() + addSubview(spinner) + spinner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top ], to: self) + spinner.startAnimating() + heightConstraint = set(.height, to: 64) + widthAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true + let _ = OpenGroupAPIV2.getDefaultRoomsPromise?.done { [weak self] rooms in + self?.rooms = rooms + } } // MARK: Updating - private func reload() { - let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(rooms.count) / 3) + private func update() { + spinner.stopAnimating() + spinner.isHidden = true + let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(rooms.count) / 2) heightConstraint.constant = height collectionView.reloadData() } // MARK: Layout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: maxWidth / 3, height: OpenGroupSuggestionGrid.cellHeight) + return CGSize(width: maxWidth / 2, height: OpenGroupSuggestionGrid.cellHeight) } // MARK: Data Source func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return min(rooms.count, 12) // Cap to a maximum of 12 (4 rows of 3) + 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.showRightSeparator = (indexPath.row % 2 != 0) || (indexPath.row % 2 == 0 && indexPath.row == rooms.count - 1) + cell.showBottomSeparator = (indexPath.row >= rooms.count - 2) 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 showRightSeparator = false + var showBottomSeparator = false var room: OpenGroupAPIV2.Info? { didSet { update() } } + private var rightSeparator: UIView! + private var bottomSeparator: UIView! static let identifier = "OpenGroupSuggestionGridCell" @@ -107,11 +133,47 @@ extension OpenGroupSuggestionGrid { label.center(in: self) label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: Values.smallSpacing).isActive = true trailingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: Values.smallSpacing).isActive = true + setUpSeparators() + } + + private func setUpSeparators() { + func getVSeparator() -> UIView { + let separator = UIView() + separator.backgroundColor = Colors.separator + separator.set(.height, to: 1 / UIScreen.main.scale) + return separator + } + func getHSeparator() -> UIView { + let separator = UIView() + separator.backgroundColor = Colors.separator + separator.set(.width, to: 1 / UIScreen.main.scale) + return separator + } + let leftSeparator = getHSeparator() + addSubview(leftSeparator) + leftSeparator.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self) + let topSeparator = getVSeparator() + addSubview(topSeparator) + topSeparator.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self) + rightSeparator = getHSeparator() + addSubview(rightSeparator) + rightSeparator.pin([ UIView.VerticalEdge.top, UIView.HorizontalEdge.right, UIView.VerticalEdge.bottom ], to: self) + bottomSeparator = getVSeparator() + addSubview(bottomSeparator) + bottomSeparator.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.right ], to: self) } private func update() { guard let room = room else { return } label.text = room.name + rightSeparator.alpha = showRightSeparator ? 1 :0 + bottomSeparator.alpha = showBottomSeparator ? 1 :0 } } } + +// MARK: Delegate +protocol OpenGroupSuggestionGridDelegate { + + func join(_ room: OpenGroupAPIV2.Info) +} diff --git a/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2+ObjC.swift b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2+ObjC.swift index 466db6303..dd9a57b18 100644 --- a/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2+ObjC.swift +++ b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2+ObjC.swift @@ -11,4 +11,9 @@ extension OpenGroupAPIV2 { public static func objc_isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { return isUserModerator(publicKey, for: room, on: server) } + + @objc(getDefaultRoomsIfNeeded) + public static func objc_getDefaultRoomsIfNeeded() { + return getDefaultRoomsIfNeeded() + } } diff --git a/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift index ddc4dd5da..b9ba34cc0 100644 --- a/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift @@ -8,6 +8,9 @@ import SessionSnodeKit @objc(SNOpenGroupAPIV2) public final class OpenGroupAPIV2 : NSObject { private static var moderators: [String:[String:Set]] = [:] // Server URL to room ID to set of moderator IDs + public static let defaultServer = "https://sessionopengroup.com" + public static let defaultServerPublicKey = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b" + public static var getDefaultRoomsPromise: Promise<[Info]>? // MARK: Error public enum Error : LocalizedError { @@ -280,6 +283,16 @@ public final class OpenGroupAPIV2 : NSObject { } // MARK: General + public static func getDefaultRoomsIfNeeded() { + Storage.shared.write(with: { transaction in + Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) + }, completion: { + getDefaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { + OpenGroupAPIV2.getAllRooms(from: defaultServer) + } + }) + } + public static func getInfo(for room: String, on server: String) -> Promise { let request = Request(verb: .get, room: room, server: server, endpoint: "rooms/\(room)", isAuthRequired: false) let promise: Promise = send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in diff --git a/SessionMessagingKit/Open Groups/V2/OpenGroupManagerV2.swift b/SessionMessagingKit/Open Groups/V2/OpenGroupManagerV2.swift index bdb6ad886..e637ad115 100644 --- a/SessionMessagingKit/Open Groups/V2/OpenGroupManagerV2.swift +++ b/SessionMessagingKit/Open Groups/V2/OpenGroupManagerV2.swift @@ -33,30 +33,34 @@ public final class OpenGroupManagerV2 : NSObject { let storage = Storage.shared storage.removeLastMessageServerID(for: room, on: server, using: transaction) storage.removeLastDeletionServerID(for: room, on: server, using: transaction) - storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) + storage.removeAuthToken(for: room, on: server, using: transaction) let (promise, seal) = Promise.pending() - OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .default)) { info in - let openGroup = OpenGroupV2(server: server, room: room, name: info.name, imageID: info.imageID) - let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) - let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) - storage.write(with: { transaction in - let transaction = transaction as! YapDatabaseReadWriteTransaction - let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.shouldThreadBeVisible = true - thread.save(with: transaction) - storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) - }, completion: { - if let poller = OpenGroupManagerV2.shared.pollers[openGroup.id] { - poller.stop() - OpenGroupManagerV2.shared.pollers[openGroup.id] = nil - } - let poller = OpenGroupPollerV2(for: openGroup) - poller.startIfNeeded() - OpenGroupManagerV2.shared.pollers[openGroup.id] = poller - seal.fulfill(()) - }) - }.catch(on: DispatchQueue.global(qos: .default)) { error in - seal.reject(error) + let transaction = transaction as! YapDatabaseReadWriteTransaction + transaction.addCompletionQueue(DispatchQueue.global(qos: .default)) { + storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) + OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .default)) { info in + let openGroup = OpenGroupV2(server: server, room: room, name: info.name, imageID: info.imageID) + let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) + let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) + storage.write(with: { transaction in + let transaction = transaction as! YapDatabaseReadWriteTransaction + let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) + thread.shouldThreadBeVisible = true + thread.save(with: transaction) + storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) + }, completion: { + if let poller = OpenGroupManagerV2.shared.pollers[openGroup.id] { + poller.stop() + OpenGroupManagerV2.shared.pollers[openGroup.id] = nil + } + let poller = OpenGroupPollerV2(for: openGroup) + poller.startIfNeeded() + OpenGroupManagerV2.shared.pollers[openGroup.id] = poller + seal.fulfill(()) + }) + }.catch(on: DispatchQueue.global(qos: .default)) { error in + seal.reject(error) + } } return promise }