Add instance selector
This commit is contained in:
parent
fc2aea04c3
commit
df8e0dedd4
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// InstanceSelector.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 9/15/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class InstanceSelector {
|
||||
|
||||
private static let decoder = JSONDecoder()
|
||||
|
||||
public static func getInstances(category: String?, completion: @escaping Client.Callback<[Instance]>) {
|
||||
let url: URL
|
||||
if let category = category {
|
||||
url = URL(string: "https://api.joinmastodon.org/servers?category=\(category)")!
|
||||
} else {
|
||||
url = URL(string: "https://api.joinmastodon.org/servers")!
|
||||
}
|
||||
let request = URLRequest(url: url)
|
||||
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let data = data,
|
||||
let response = response as? HTTPURLResponse else {
|
||||
completion(.failure(Client.Error.invalidResponse))
|
||||
return
|
||||
}
|
||||
guard response.statusCode == 200 else {
|
||||
completion(.failure(Client.Error.unknownError))
|
||||
return
|
||||
}
|
||||
guard let result = try? decoder.decode([Instance].self, from: data) else {
|
||||
completion(.failure(Client.Error.invalidModel))
|
||||
return
|
||||
}
|
||||
completion(.success(result, nil))
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension InstanceSelector {
|
||||
struct Instance: Codable {
|
||||
public let domain: String
|
||||
public let description: String
|
||||
public let proxiedThumbnailURL: URL
|
||||
public let language: String
|
||||
public let category: Category
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case domain
|
||||
case description
|
||||
case proxiedThumbnailURL = "proxied_thumbnail"
|
||||
case language
|
||||
case category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstanceSelector {
|
||||
enum Category: String, Codable {
|
||||
// source: https://source.joinmastodon.org/mastodon/joinmastodon/blob/master/src/Wizard.js#L108
|
||||
case general
|
||||
case regional
|
||||
case art
|
||||
case journalism
|
||||
case activism
|
||||
case lgbt
|
||||
case games
|
||||
case tech
|
||||
case adult
|
||||
case furry
|
||||
case food
|
||||
}
|
||||
}
|
|
@ -72,6 +72,10 @@
|
|||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
|
||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */; };
|
||||
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; };
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF75217E923E00CC0648 /* DraftsManager.swift */; };
|
||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF78217E950100CC0648 /* DraftsTableViewController.xib */; };
|
||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */; };
|
||||
|
@ -157,7 +161,6 @@
|
|||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; };
|
||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */; };
|
||||
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; };
|
||||
D6A5FAFB217B86CE003DB2D9 /* OnboardingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */; };
|
||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
|
||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
|
||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
|
||||
|
@ -327,6 +330,10 @@
|
|||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
|
||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsManager.swift; sourceTree = "<group>"; };
|
||||
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftsTableViewController.xib; sourceTree = "<group>"; };
|
||||
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsTableViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -411,7 +418,6 @@
|
|||
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6A3BC8D2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; };
|
||||
D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardingViewController.xib; sourceTree = "<group>"; };
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
|
||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||
|
@ -674,6 +680,15 @@
|
|||
path = "Hashtag Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D61AC1DA232EA43100C54D2D /* Instance Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */,
|
||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */,
|
||||
);
|
||||
path = "Instance Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D627FF77217E94F200CC0648 /* Drafts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -737,8 +752,8 @@
|
|||
D641C783213DD7FE004B4513 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */,
|
||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */,
|
||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
|
@ -980,6 +995,7 @@
|
|||
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */,
|
||||
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */,
|
||||
D667383B23299340000A2373 /* InstanceType.swift */,
|
||||
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1055,6 +1071,7 @@
|
|||
D641C78C213DD937004B4513 /* Notifications */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
||||
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1418,6 +1435,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||
|
@ -1425,7 +1443,6 @@
|
|||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||
D6A5FAFB217B86CE003DB2D9 /* OnboardingViewController.xib in Resources */,
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
||||
|
@ -1478,6 +1495,7 @@
|
|||
D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */,
|
||||
D61099D92144B76400432DC2 /* Data.swift in Sources */,
|
||||
D61099EB2145661700432DC2 /* ConversationContext.swift in Sources */,
|
||||
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */,
|
||||
D61099C92144B13C00432DC2 /* Client.swift in Sources */,
|
||||
D61099D42144B32E00432DC2 /* Parameter.swift in Sources */,
|
||||
D61099CB2144B20500432DC2 /* Request.swift in Sources */,
|
||||
|
@ -1587,6 +1605,7 @@
|
|||
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */,
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||
|
@ -1610,6 +1629,7 @@
|
|||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||
|
|
|
@ -73,7 +73,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
func showOnboardingUI() {
|
||||
let onboarding = OnboardingViewController()
|
||||
onboarding.delegate = self
|
||||
onboarding.onboardingDelegate = self
|
||||
window!.rootViewController = onboarding
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
//
|
||||
// InstanceSelectorTableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/15/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
protocol InstanceSelectorTableViewControllerDelegate {
|
||||
func didSelectInstance(url: URL)
|
||||
}
|
||||
|
||||
fileprivate let instanceCell = "instanceCell"
|
||||
|
||||
class InstanceSelectorTableViewController: UITableViewController {
|
||||
|
||||
var delegate: InstanceSelectorTableViewControllerDelegate?
|
||||
|
||||
var dataSource: DataSource!
|
||||
var searchController: UISearchController!
|
||||
|
||||
var recommendedInstances: [InstanceSelector.Instance] = []
|
||||
|
||||
let urlCheckerSubject = PassthroughSubject<String?, Never>()
|
||||
var currentQuery: String?
|
||||
|
||||
init() {
|
||||
super.init(style: .grouped)
|
||||
|
||||
title = NSLocalizedString("Choose Your Instance", comment: "onboarding screen title")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 120
|
||||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
case let .selected(instance):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||
cell.updateUI(instance: instance)
|
||||
cell.delegate = self
|
||||
return cell
|
||||
case let .recommended(instance):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||
cell.updateUI(instance: instance)
|
||||
cell.delegate = self
|
||||
return cell
|
||||
}
|
||||
})
|
||||
|
||||
searchController = UISearchController(searchResultsController: nil)
|
||||
searchController.searchResultsUpdater = self
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||
navigationItem.searchController = searchController
|
||||
definesPresentationContext = true
|
||||
|
||||
_ = urlCheckerSubject
|
||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.sink(receiveValue: updateSpecificInstance)
|
||||
|
||||
loadRecommendedInstances()
|
||||
}
|
||||
|
||||
private func updateSpecificInstance(domain: String) {
|
||||
var components = URLComponents()
|
||||
if domain.contains("://") {
|
||||
let parts = domain.components(separatedBy: "://")
|
||||
components.scheme = parts[0]
|
||||
components.host = parts[1]
|
||||
} else {
|
||||
components.host = domain
|
||||
}
|
||||
if components.scheme != "https" && components.scheme != "http" {
|
||||
components.scheme = "https"
|
||||
}
|
||||
components.path = "/"
|
||||
|
||||
let client = Client(baseURL: components.url!)
|
||||
let request = client.getInstance()
|
||||
client.run(request) { (response) in
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
||||
|
||||
if case let .success(instance, _) = response {
|
||||
if !snapshot.sectionIdentifiers.contains(.selected) {
|
||||
snapshot.appendSections([.selected])
|
||||
}
|
||||
snapshot.appendItems([.selected(instance)], toSection: .selected)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadRecommendedInstances() {
|
||||
InstanceSelector.getInstances(category: nil) { (response) in
|
||||
guard case let .success(instances, _) = response else { fatalError() }
|
||||
|
||||
self.recommendedInstances = instances
|
||||
self.filterRecommendedResults()
|
||||
}
|
||||
}
|
||||
|
||||
func filterRecommendedResults() {
|
||||
let filteredInstances: [InstanceSelector.Instance]
|
||||
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
||||
filteredInstances = recommendedInstances.filter {
|
||||
$0.domain.contains(currentQuery) || $0.description.lowercased().contains(currentQuery)
|
||||
}
|
||||
} else {
|
||||
filteredInstances = recommendedInstances
|
||||
}
|
||||
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteSections([.recommendedInstances])
|
||||
snapshot.appendSections([.recommendedInstances])
|
||||
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceSelectorTableViewController {
|
||||
enum Section {
|
||||
case selected
|
||||
case recommendedInstances
|
||||
}
|
||||
enum Item: Equatable, Hashable {
|
||||
case selected(Instance)
|
||||
case recommended(InstanceSelector.Instance)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if case let .selected(instance) = lhs,
|
||||
case let .selected(other) = rhs {
|
||||
return instance.uri == other.uri
|
||||
} else if case let .recommended(instance) = lhs,
|
||||
case let .recommended(other) = rhs {
|
||||
return instance.domain == other.domain
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .selected(instance):
|
||||
hasher.combine(Section.selected)
|
||||
hasher.combine(instance.uri)
|
||||
case let .recommended(instance):
|
||||
hasher.combine(Section.recommendedInstances)
|
||||
hasher.combine(instance.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceSelectorTableViewController: UISearchResultsUpdating {
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
currentQuery = searchController.searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
filterRecommendedResults()
|
||||
urlCheckerSubject.send(currentQuery)
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceSelectorTableViewController: InstanceTableViewCellDelegate {
|
||||
func didSelectInstance(_ instance: Instance) {
|
||||
delegate?.didSelectInstance(url: URL(string: instance.uri)!)
|
||||
}
|
||||
func didSelectInstance(_ instance: InstanceSelector.Instance) {
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
components.host = instance.domain
|
||||
components.path = "/"
|
||||
delegate?.didSelectInstance(url: components.url!)
|
||||
}
|
||||
}
|
|
@ -10,12 +10,10 @@ import UIKit
|
|||
import AuthenticationServices
|
||||
|
||||
protocol OnboardingViewControllerDelegate {
|
||||
|
||||
func didFinishOnboarding()
|
||||
|
||||
}
|
||||
|
||||
class OnboardingViewController: UIViewController {
|
||||
class OnboardingViewController: UINavigationController {
|
||||
|
||||
static var blocks: [NSRegularExpression] = {
|
||||
guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
|
||||
|
@ -23,14 +21,14 @@ class OnboardingViewController: UIViewController {
|
|||
return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
|
||||
}()
|
||||
|
||||
var delegate: OnboardingViewControllerDelegate?
|
||||
var onboardingDelegate: OnboardingViewControllerDelegate?
|
||||
|
||||
@IBOutlet weak var urlTextField: UITextField!
|
||||
var instanceSelector = InstanceSelectorTableViewController()
|
||||
|
||||
var authenticationSession: ASWebAuthenticationSession?
|
||||
|
||||
init() {
|
||||
super.init(nibName: "OnboardingViewController", bundle: nil)
|
||||
super.init(rootViewController: instanceSelector)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
@ -39,21 +37,14 @@ class OnboardingViewController: UIViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
instanceSelector.delegate = self
|
||||
}
|
||||
|
||||
@IBAction func loginPressed(_ sender: Any) {
|
||||
guard let text = urlTextField.text,
|
||||
let url = URL(string: text),
|
||||
var components = URLComponents(string: text),
|
||||
let host = components.host,
|
||||
!OnboardingViewController.blocks.contains(where: {
|
||||
$0.numberOfMatches(in: host, range: NSRange(host.startIndex..<host.endIndex, in: host)) > 0
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
|
||||
func didSelectInstance(url: URL) {
|
||||
LocalData.shared.instanceURL = url
|
||||
MastodonController.createClient()
|
||||
MastodonController.registerApp {
|
||||
|
@ -61,6 +52,7 @@ class OnboardingViewController: UIViewController {
|
|||
|
||||
let callbackURL = "tusker://oauth"
|
||||
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
|
||||
components.path = "/oauth/authorize"
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
|
@ -68,9 +60,9 @@ class OnboardingViewController: UIViewController {
|
|||
URLQueryItem(name: "scope", value: "read write follow"),
|
||||
URLQueryItem(name: "redirect_uri", value: callbackURL)
|
||||
]
|
||||
let url = components.url!
|
||||
let authorizeURL = components.url!
|
||||
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL) { url, error in
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: callbackURL) { url, error in
|
||||
guard error == nil,
|
||||
let url = url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
|
@ -79,7 +71,7 @@ class OnboardingViewController: UIViewController {
|
|||
|
||||
MastodonController.authorize(authorizationCode: authCode) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.didFinishOnboarding()
|
||||
self.onboardingDelegate?.didFinishOnboarding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,14 +81,6 @@ class OnboardingViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func clearDataPressed(_ sender: Any) {
|
||||
LocalData.shared.instanceURL = nil
|
||||
LocalData.shared.clientID = nil
|
||||
LocalData.shared.clientSecret = nil
|
||||
LocalData.shared.accessToken = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OnboardingViewController: ASWebAuthenticationPresentationContextProviding {
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14810.11" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.13"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
<capability name="iOS 13.0 system colors" minToolsVersion="11.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="OnboardingViewController" customModule="Tusker" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="urlTextField" destination="6hF-SD-ZQb" id="RFc-LM-yGe"/>
|
||||
<outlet property="view" destination="R4s-4d-feU" id="Aru-mx-JeN"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="R4s-4d-feU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="vSP-TA-ZO7">
|
||||
<rect key="frame" x="41.5" y="246.5" width="292.5" height="174.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter your instance URL to get started" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BjT-C7-f5D">
|
||||
<rect key="frame" x="0.0" y="0.0" width="292.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="https://mastodon.social" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="6hF-SD-ZQb">
|
||||
<rect key="frame" x="0.0" y="40.5" width="292.5" height="34"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="secondarySystemBackgroundColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="WLU-Sm-Bxo">
|
||||
<rect key="frame" x="0.0" y="94.5" width="292.5" height="30"/>
|
||||
<state key="normal" title="Login"/>
|
||||
<connections>
|
||||
<action selector="loginPressed:" destination="-1" eventType="touchUpInside" id="wHH-gO-nPZ"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="83X-5h-PM4">
|
||||
<rect key="frame" x="0.0" y="144.5" width="292.5" height="30"/>
|
||||
<state key="normal" title="Clear Data"/>
|
||||
<connections>
|
||||
<action selector="clearDataPressed:" destination="-1" eventType="touchUpInside" id="txX-4P-jE7"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vSP-TA-ZO7" firstAttribute="centerX" secondItem="R4s-4d-feU" secondAttribute="centerX" id="Rts-gb-MB0"/>
|
||||
<constraint firstItem="vSP-TA-ZO7" firstAttribute="centerY" secondItem="R4s-4d-feU" secondAttribute="centerY" id="fMM-cG-1Cb"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="gaH-lv-6pD"/>
|
||||
<point key="canvasLocation" x="142" y="154"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// InstanceTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/15/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
protocol InstanceTableViewCellDelegate {
|
||||
func didSelectInstance(_ instance: Instance)
|
||||
func didSelectInstance(_ instance: InstanceSelector.Instance)
|
||||
}
|
||||
|
||||
class InstanceTableViewCell: UITableViewCell {
|
||||
|
||||
var delegate: InstanceTableViewCellDelegate?
|
||||
|
||||
@IBOutlet weak var thumbnailImageView: UIImageView!
|
||||
@IBOutlet weak var domainLabel: UILabel!
|
||||
@IBOutlet weak var adultLabel: UILabel!
|
||||
@IBOutlet weak var descriptionLabel: ContentLabel!
|
||||
|
||||
var instance: Instance?
|
||||
var selectorInstance: InstanceSelector.Instance?
|
||||
|
||||
var thumbnailURL: URL?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
thumbnailImageView.layer.masksToBounds = true
|
||||
thumbnailImageView.layer.cornerRadius = 5
|
||||
|
||||
adultLabel.layer.masksToBounds = true
|
||||
adultLabel.layer.cornerRadius = 0.5 * adultLabel.bounds.height
|
||||
}
|
||||
|
||||
func updateUI(instance: InstanceSelector.Instance) {
|
||||
self.selectorInstance = instance
|
||||
self.instance = nil
|
||||
|
||||
domainLabel.text = instance.domain
|
||||
adultLabel.isHidden = instance.category != .adult
|
||||
descriptionLabel.setTextFromHtml(instance.description)
|
||||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||
}
|
||||
|
||||
func updateUI(instance: Instance) {
|
||||
self.instance = instance
|
||||
self.selectorInstance = nil
|
||||
|
||||
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
||||
adultLabel.isHidden = true
|
||||
descriptionLabel.setTextFromHtml(instance.description)
|
||||
|
||||
if let thumbnail = instance.thumbnail {
|
||||
updateThumbnail(url: thumbnail)
|
||||
} else {
|
||||
thumbnailImageView.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateThumbnail(url: URL) {
|
||||
thumbnailImageView.image = nil
|
||||
thumbnailURL = url
|
||||
ImageCache.attachments.get(url) { (data) in
|
||||
guard self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.thumbnailImageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
if selected, let delegate = delegate {
|
||||
if let instance = instance {
|
||||
delegate.didSelectInstance(instance)
|
||||
} else if let instance = selectorInstance {
|
||||
delegate.didSelectInstance(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="120" id="KGk-i7-Jjw" customClass="InstanceTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="120"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="120"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="H4g-D1-WTr">
|
||||
<rect key="frame" x="16" y="11" width="288" height="98"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="e2C-wt-pkK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="60" id="1HP-TQ-xFZ"/>
|
||||
<constraint firstAttribute="width" constant="80" id="qlF-tY-b1W"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="QG1-xB-nmt">
|
||||
<rect key="frame" x="88" y="0.0" width="200" height="47"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="XtJ-BL-iHb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="26.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="domain.tld" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SjP-Nk-sSH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="164" height="26.5"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="18+" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ekk-aL-7Pq">
|
||||
<rect key="frame" x="164" y="1.5" width="36" height="24"/>
|
||||
<color key="backgroundColor" systemColor="systemBlueColor" red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="24" id="CNa-UL-LJh"/>
|
||||
<constraint firstAttribute="width" constant="36" id="tzn-KZ-r2f"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Instance Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aD6-LG-BWG" customClass="ContentLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="26.5" width="200" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="H4g-D1-WTr" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="eJu-AI-eBw"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="H4g-D1-WTr" secondAttribute="bottom" id="gIC-zx-CfT"/>
|
||||
<constraint firstItem="H4g-D1-WTr" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="qNO-nz-Buy"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="H4g-D1-WTr" secondAttribute="trailing" id="xTY-Rm-P02"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="adultLabel" destination="ekk-aL-7Pq" id="vzP-Gm-QF7"/>
|
||||
<outlet property="descriptionLabel" destination="aD6-LG-BWG" id="KNk-Gq-cDU"/>
|
||||
<outlet property="domainLabel" destination="SjP-Nk-sSH" id="QPQ-n6-yoK"/>
|
||||
<outlet property="thumbnailImageView" destination="e2C-wt-pkK" id="KeP-Xf-0Tn"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="131.8840579710145" y="178.79464285714286"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
Loading…
Reference in New Issue