Hook up suggestion grid

This commit is contained in:
Niels Andriesse 2021-03-26 13:28:40 +11:00
parent 053f581bb2
commit a7dd7e1bf0
7 changed files with 164 additions and 42 deletions

View File

@ -153,6 +153,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
let _ = IP2Country.shared.populateCacheIfNeeded() let _ = IP2Country.shared.populateCacheIfNeeded()
} }
// Get default open group rooms if needed
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {

View File

@ -396,7 +396,7 @@ static NSTimeInterval launchStartedAt;
[self startClosedGroupPollerIfNeeded]; [self startClosedGroupPollerIfNeeded];
[self startOpenGroupPollersIfNeeded]; [self startOpenGroupPollersIfNeeded];
// Loki: Update profile picture if needed // Update profile picture if needed
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
NSDate *now = [NSDate new]; NSDate *now = [NSDate new];
NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"]; NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
@ -410,6 +410,10 @@ static NSTimeInterval launchStartedAt;
// Do nothing // Do nothing
} requiresSync:YES]; } requiresSync:YES];
} }
if (CurrentAppContext().isMainApp) {
[SNOpenGroupAPIV2 getDefaultRoomsIfNeeded];
}
if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
OWSLogInfo(@"Retrying remote notification registration since user hasn't registered yet."); OWSLogInfo(@"Retrying remote notification registration since user hasn't registered yet.");

View File

@ -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 // MARK: Convenience
private func showError(title: String, message: String = "") { private func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 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! weak var joinOpenGroupVC: JoinOpenGroupVC!
// MARK: Components // MARK: Components
@ -170,13 +192,22 @@ private final class EnterURLVC : UIViewController {
private lazy var suggestionGrid: OpenGroupSuggestionGrid = { private lazy var suggestionGrid: OpenGroupSuggestionGrid = {
let maxWidth = UIScreen.main.bounds.width - Values.largeSpacing * 2 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 // MARK: Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
// Remove background color // Remove background color
view.backgroundColor = .clear 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 // Next button
let nextButton = Button(style: .prominentOutline, size: .large) let nextButton = Button(style: .prominentOutline, size: .large)
nextButton.setTitle(NSLocalizedString("next", comment: ""), for: UIControl.State.normal) 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) nextButton.pin(.top, to: .top, of: nextButtonContainer)
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80) nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton) nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
// Spacers
let spacer1 = UIView.vStretchingSpacer()
let spacer2 = UIView.vStretchingSpacer()
// Stack view // 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.axis = .vertical
stackView.alignment = .fill stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(uniform: Values.largeSpacing) stackView.layoutMargins = UIEdgeInsets(uniform: Values.largeSpacing)
@ -200,7 +229,6 @@ private final class EnterURLVC : UIViewController {
stackView.pin(to: view) stackView.pin(to: view)
// Constraints // Constraints
view.set(.width, to: UIScreen.main.bounds.width) view.set(.width, to: UIScreen.main.bounds.width)
spacer1.heightAnchor.constraint(equalTo: spacer2.heightAnchor).isActive = true
// Dismiss keyboard on tap // Dismiss keyboard on tap
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGestureRecognizer) view.addGestureRecognizer(tapGestureRecognizer)
@ -216,6 +244,10 @@ private final class EnterURLVC : UIViewController {
} }
// MARK: Interaction // MARK: Interaction
func join(_ room: OpenGroupAPIV2.Info) {
joinOpenGroupVC.join(room.id, on: OpenGroupAPIV2.defaultServer, with: OpenGroupAPIV2.defaultServerPublicKey)
}
@objc private func joinOpenGroupIfPossible() { @objc private func joinOpenGroupIfPossible() {
let url = urlTextField.text?.trimmingCharacters(in: .whitespaces) ?? "" let url = urlTextField.text?.trimmingCharacters(in: .whitespaces) ?? ""
joinOpenGroupVC.joinOpenGroupIfPossible(with: url) joinOpenGroupVC.joinOpenGroupIfPossible(with: url)

View File

@ -1,8 +1,11 @@
import PromiseKit
import NVActivityIndicatorView
final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private let maxWidth: CGFloat private let maxWidth: CGFloat
private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { reload() } } private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { update() } }
private var heightConstraint: NSLayoutConstraint! private var heightConstraint: NSLayoutConstraint!
var delegate: OpenGroupSuggestionGridDelegate?
// MARK: UI Components // MARK: UI Components
private lazy var layout: UICollectionViewFlowLayout = { private lazy var layout: UICollectionViewFlowLayout = {
@ -22,6 +25,13 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl
return result 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 // MARK: Settings
private static let cellHeight: CGFloat = 40 private static let cellHeight: CGFloat = 40
private static let separatorWidth = 1 / UIScreen.main.scale private static let separatorWidth = 1 / UIScreen.main.scale
@ -44,43 +54,59 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl
private func initialize() { private func initialize() {
addSubview(collectionView) addSubview(collectionView)
collectionView.pin(to: self) collectionView.pin(to: self)
heightConstraint = set(.height, to: 0) addSubview(spinner)
attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { spinner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top ], to: self)
return OpenGroupAPIV2.getAllRooms(from: "https://sessionopengroup.com").done { [weak self] rooms in spinner.startAnimating()
self?.rooms = rooms heightConstraint = set(.height, to: 64)
} widthAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true
}.retainUntilComplete() let _ = OpenGroupAPIV2.getDefaultRoomsPromise?.done { [weak self] rooms in
self?.rooms = rooms
}
} }
// MARK: Updating // MARK: Updating
private func reload() { private func update() {
let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(rooms.count) / 3) spinner.stopAnimating()
spinner.isHidden = true
let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(rooms.count) / 2)
heightConstraint.constant = height heightConstraint.constant = height
collectionView.reloadData() collectionView.reloadData()
} }
// MARK: Layout // MARK: Layout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 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 // MARK: Data Source
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 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 { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell 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] cell.room = rooms[indexPath.item]
return cell return cell
} }
// MARK: Interaction
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let room = rooms[indexPath.item]
delegate?.join(room)
}
} }
// MARK: Cell // MARK: Cell
extension OpenGroupSuggestionGrid { extension OpenGroupSuggestionGrid {
fileprivate final class Cell : UICollectionViewCell { fileprivate final class Cell : UICollectionViewCell {
var showRightSeparator = false
var showBottomSeparator = false
var room: OpenGroupAPIV2.Info? { didSet { update() } } var room: OpenGroupAPIV2.Info? { didSet { update() } }
private var rightSeparator: UIView!
private var bottomSeparator: UIView!
static let identifier = "OpenGroupSuggestionGridCell" static let identifier = "OpenGroupSuggestionGridCell"
@ -107,11 +133,47 @@ extension OpenGroupSuggestionGrid {
label.center(in: self) label.center(in: self)
label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: Values.smallSpacing).isActive = true label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: Values.smallSpacing).isActive = true
trailingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, 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() { private func update() {
guard let room = room else { return } guard let room = room else { return }
label.text = room.name label.text = room.name
rightSeparator.alpha = showRightSeparator ? 1 :0
bottomSeparator.alpha = showBottomSeparator ? 1 :0
} }
} }
} }
// MARK: Delegate
protocol OpenGroupSuggestionGridDelegate {
func join(_ room: OpenGroupAPIV2.Info)
}

View File

@ -11,4 +11,9 @@ extension OpenGroupAPIV2 {
public static func objc_isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { public static func objc_isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool {
return isUserModerator(publicKey, for: room, on: server) return isUserModerator(publicKey, for: room, on: server)
} }
@objc(getDefaultRoomsIfNeeded)
public static func objc_getDefaultRoomsIfNeeded() {
return getDefaultRoomsIfNeeded()
}
} }

View File

@ -8,6 +8,9 @@ import SessionSnodeKit
@objc(SNOpenGroupAPIV2) @objc(SNOpenGroupAPIV2)
public final class OpenGroupAPIV2 : NSObject { public final class OpenGroupAPIV2 : NSObject {
private static var moderators: [String:[String:Set<String>]] = [:] // Server URL to room ID to set of moderator IDs private static var moderators: [String:[String:Set<String>]] = [:] // 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 // MARK: Error
public enum Error : LocalizedError { public enum Error : LocalizedError {
@ -280,6 +283,16 @@ public final class OpenGroupAPIV2 : NSObject {
} }
// MARK: General // 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<Info> { public static func getInfo(for room: String, on server: String) -> Promise<Info> {
let request = Request(verb: .get, room: room, server: server, endpoint: "rooms/\(room)", isAuthRequired: false) let request = Request(verb: .get, room: room, server: server, endpoint: "rooms/\(room)", isAuthRequired: false)
let promise: Promise<Info> = send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in let promise: Promise<Info> = send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in

View File

@ -33,30 +33,34 @@ public final class OpenGroupManagerV2 : NSObject {
let storage = Storage.shared let storage = Storage.shared
storage.removeLastMessageServerID(for: room, on: server, using: transaction) storage.removeLastMessageServerID(for: room, on: server, using: transaction)
storage.removeLastDeletionServerID(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<Void>.pending() let (promise, seal) = Promise<Void>.pending()
OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .default)) { info in let transaction = transaction as! YapDatabaseReadWriteTransaction
let openGroup = OpenGroupV2(server: server, room: room, name: info.name, imageID: info.imageID) transaction.addCompletionQueue(DispatchQueue.global(qos: .default)) {
let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .default)) { info in
storage.write(with: { transaction in let openGroup = OpenGroupV2(server: server, room: room, name: info.name, imageID: info.imageID)
let transaction = transaction as! YapDatabaseReadWriteTransaction let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: [])
thread.shouldThreadBeVisible = true storage.write(with: { transaction in
thread.save(with: transaction) let transaction = transaction as! YapDatabaseReadWriteTransaction
storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
}, completion: { thread.shouldThreadBeVisible = true
if let poller = OpenGroupManagerV2.shared.pollers[openGroup.id] { thread.save(with: transaction)
poller.stop() storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
OpenGroupManagerV2.shared.pollers[openGroup.id] = nil }, completion: {
} if let poller = OpenGroupManagerV2.shared.pollers[openGroup.id] {
let poller = OpenGroupPollerV2(for: openGroup) poller.stop()
poller.startIfNeeded() OpenGroupManagerV2.shared.pollers[openGroup.id] = nil
OpenGroupManagerV2.shared.pollers[openGroup.id] = poller }
seal.fulfill(()) let poller = OpenGroupPollerV2(for: openGroup)
}) poller.startIfNeeded()
}.catch(on: DispatchQueue.global(qos: .default)) { error in OpenGroupManagerV2.shared.pollers[openGroup.id] = poller
seal.reject(error) seal.fulfill(())
})
}.catch(on: DispatchQueue.global(qos: .default)) { error in
seal.reject(error)
}
} }
return promise return promise
} }