Add instance selector

This commit is contained in:
Shadowfacts 2019-09-15 15:01:35 -04:00
parent fc2aea04c3
commit df8e0dedd4
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 486 additions and 99 deletions

View File

@ -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
}
}

View File

@ -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 */,

View File

@ -73,7 +73,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func showOnboardingUI() {
let onboarding = OnboardingViewController()
onboarding.delegate = self
onboarding.onboardingDelegate = self
window!.rootViewController = onboarding
}

View File

@ -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!)
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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)
}
}
}
}

View File

@ -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>