Store an array of logged-in accounts internally, get the active

MastodonController from the current UIScene

See #16
This commit is contained in:
Shadowfacts 2020-01-07 21:29:15 -05:00
parent 8dba15ca17
commit 3928b2e88a
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
14 changed files with 225 additions and 102 deletions

View File

@ -217,6 +217,8 @@
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
@ -494,6 +496,8 @@
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
@ -966,6 +970,8 @@
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */,
0450531E22B0097E00100BA2 /* Timline+UI.swift */,
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */,
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1620,6 +1626,7 @@
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
@ -1680,6 +1687,7 @@
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,

View File

@ -10,6 +10,7 @@ import UIKit
class MastodonActivity: UIActivity {
var mastodonController: MastodonController {
MastodonController.shared
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
}

View File

@ -10,55 +10,67 @@ import Foundation
import Pachyderm
class MastodonController {
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
@available(*, deprecated, message: "Use dependency injection to obtain an instance")
static let shared = MastodonController()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController {
if let controller = all[account] {
return controller
} else {
let controller = MastodonController(instanceURL: account.instanceURL)
controller.accountInfo = account
controller.client.clientID = account.clientID
controller.client.clientSecret = account.clientSecret
controller.client.accessToken = account.accessToken
all[account] = controller
return controller
}
}
private(set) lazy var cache = MastodonCache(mastodonController: self)
private var client: Client!
let instanceURL: URL
private(set) var accountInfo: LocalData.UserAccountInfo?
let client: Client!
var account: Account!
var instance: Instance!
var accessToken: String? {
client?.accessToken
}
func createClient(instanceURL: URL = LocalData.shared.instanceURL!) {
client = Client(baseURL: instanceURL)
if instanceURL == LocalData.shared.instanceURL {
client.clientID = LocalData.shared.clientID
client.clientSecret = LocalData.shared.clientSecret
client.accessToken = LocalData.shared.accessToken
}
init(instanceURL: URL) {
self.instanceURL = instanceURL
self.accountInfo = nil
self.client = Client(baseURL: instanceURL)
}
func run<Result>(_ request: Request<Result>, completion: @escaping Client.Callback<Result>) {
client.run(request, completion: completion)
}
func registerApp(completion: @escaping () -> Void) {
guard LocalData.shared.clientID == nil,
LocalData.shared.clientSecret == nil else {
completion()
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
guard client.clientID == nil,
client.clientSecret == nil else {
completion(client.clientID!, client.clientSecret!)
return
}
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in
guard case let .success(app, _) = response else { fatalError() }
LocalData.shared.clientID = app.clientID
LocalData.shared.clientSecret = app.clientSecret
completion()
self.client.clientID = app.clientID
self.client.clientSecret = app.clientSecret
completion(app.clientID, app.clientSecret)
}
}
func authorize(authorizationCode: String, completion: @escaping () -> Void) {
func authorize(authorizationCode: String, completion: @escaping (_ accessToken: String) -> Void) {
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in
guard case let .success(settings, _) = response else { fatalError() }
LocalData.shared.accessToken = settings.accessToken
completion()
self.client.accessToken = settings.accessToken
completion(settings.accessToken)
}
}

View File

@ -0,0 +1,25 @@
//
// UIApplication+Scenes.swift
// Tusker
//
// Created by Shadowfacts on 1/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
extension UIApplication {
var activeScene: UIScene? {
connectedScenes.first { $0.activationState == .foregroundActive }
}
var backgroundScene: UIScene? {
connectedScenes.first { $0.activationState == .background }
}
var activeOrBackgroundScene: UIScene? {
activeScene ?? backgroundScene
}
}

View File

@ -0,0 +1,32 @@
//
// UISceneSession+MastodonController.swift
// Tusker
//
// Created by Shadowfacts on 1/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
extension UISceneSession {
var mastodonController: MastodonController? {
get {
return userInfo?["mastodonController"] as? MastodonController
}
set {
if let newValue = newValue {
if userInfo == nil {
userInfo = ["mastodonController": newValue]
} else {
userInfo!["mastodonController"] = newValue
}
} else {
if userInfo != nil {
userInfo?.removeValue(forKey: "mastodonController")
}
}
}
}
}

View File

@ -18,65 +18,99 @@ class LocalData {
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") {
defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")!
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
defaults.set(true, forKey: onboardingCompleteKey)
defaults.set(URL(string: "http://localhost:8080")!, forKey: instanceURLKey)
defaults.set("client_id", forKey: clientIDKey)
defaults.set("client_secret", forKey: clientSecretKey)
defaults.set("access_token", forKey: accessTokenKey)
accounts = [
UserAccountInfo(
instanceURL: URL(string: "http://localhost:8080")!,
clientID: "client_id",
clientSecret: "client_secret",
username: "admin",
accessToken: "access_token")
]
}
} else {
defaults = UserDefaults()
}
}
private let onboardingCompleteKey = "onboardingComplete"
private let accountsKey = "accounts"
var accounts: [UserAccountInfo] {
get {
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
return array.compactMap { (info) in
guard let instanceURL = info["instanceURL"],
let url = URL(string: instanceURL),
let id = info["clientID"],
let secret = info["clientSecret"],
let username = info["username"],
let accessToken = info["accessToken"] else {
return nil
}
return UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken)
}
} else {
return []
}
}
set {
let array = newValue.map { (info) in
return [
"instanceURL": info.instanceURL.absoluteString,
"clientID": info.clientID,
"clientSecret": info.clientSecret,
"username": info.username,
"accessToken": info.accessToken
]
}
defaults.set(array, forKey: accountsKey)
}
}
private let mostRecentAccountKey = "mostRecentAccount"
var mostRecentAccount: String? {
get {
return defaults.string(forKey: mostRecentAccountKey)
}
set {
defaults.set(newValue, forKey: mostRecentAccountKey)
}
}
var onboardingComplete: Bool {
get {
return defaults.bool(forKey: onboardingCompleteKey)
}
set {
defaults.set(newValue, forKey: onboardingCompleteKey)
}
return !accounts.isEmpty
}
private let instanceURLKey = "instanceURL"
var instanceURL: URL? {
get {
return defaults.url(forKey: instanceURLKey)
}
set {
defaults.set(newValue, forKey: instanceURLKey)
func addAccount(instanceURL url: URL, clientID id: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
var accounts = self.accounts
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
accounts.remove(at: index)
}
let info = UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken)
accounts.append(info)
self.accounts = accounts
return info
}
private let clientIDKey = "clientID"
var clientID: String? {
get {
return defaults.string(forKey: clientIDKey)
}
set {
defaults.set(newValue, forKey: clientIDKey)
}
func removeAccount(_ info: UserAccountInfo) {
}
private let clientSecretKey = "clientSecret"
var clientSecret: String? {
get {
return defaults.string(forKey: clientSecretKey)
}
set {
defaults.set(newValue, forKey: clientSecretKey)
func getMostRecentAccount() -> UserAccountInfo? {
if let accessToken = mostRecentAccount {
return accounts.first { $0.accessToken == accessToken }
} else {
return nil
}
}
private let accessTokenKey = "accessToken"
var accessToken: String? {
get {
return defaults.string(forKey: accessTokenKey)
}
set {
defaults.set(newValue, forKey: accessTokenKey)
}
}
extension LocalData {
struct UserAccountInfo: Equatable, Hashable {
let instanceURL: URL
let clientID: String
let clientSecret: String
let username: String
let accessToken: String
}
}

View File

@ -7,12 +7,13 @@
//
import UIKit
import Pachyderm
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let mastodonController = MastodonController.shared
// let mastodonController = MastodonController.shared
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
@ -23,6 +24,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window = UIWindow(windowScene: windowScene)
if LocalData.shared.onboardingComplete {
if session.mastodonController == nil {
let account = LocalData.shared.getMostRecentAccount()!
session.mastodonController = MastodonController.getForAccount(account)
}
showAppUI()
} else {
showOnboardingUI()
@ -106,7 +112,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
func showAppUI() {
mastodonController.createClient()
let mastodonController = window!.windowScene!.session.mastodonController!
mastodonController.getOwnAccount()
mastodonController.getOwnInstance()
@ -131,8 +137,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
extension SceneDelegate: OnboardingViewControllerDelegate {
func didFinishOnboarding() {
LocalData.shared.onboardingComplete = true
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
LocalData.shared.mostRecentAccount = account.accessToken
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
showAppUI()
}
}

View File

@ -10,7 +10,7 @@ import UIKit
import AuthenticationServices
protocol OnboardingViewControllerDelegate {
func didFinishOnboarding()
func didFinishOnboarding(account: LocalData.UserAccountInfo)
}
class OnboardingViewController: UINavigationController {
@ -44,16 +44,13 @@ class OnboardingViewController: UINavigationController {
}
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
func didSelectInstance(url: URL) {
LocalData.shared.instanceURL = url
let mastodonController = MastodonController.shared
mastodonController.createClient()
mastodonController.registerApp {
let clientID = LocalData.shared.clientID!
func didSelectInstance(url instanceURL: URL) {
let mastodonController = MastodonController(instanceURL: instanceURL)
mastodonController.registerApp { (clientID, clientSecret) in
let callbackURL = "tusker://oauth"
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
components.path = "/oauth/authorize"
components.queryItems = [
URLQueryItem(name: "client_id", value: clientID),
@ -70,9 +67,13 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
let item = components.queryItems?.first(where: { $0.name == "code" }),
let authCode = item.value else { return }
mastodonController.authorize(authorizationCode: authCode) {
DispatchQueue.main.async {
self.onboardingDelegate?.didFinishOnboarding()
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
mastodonController.getOwnAccount { (account) in
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
DispatchQueue.main.async {
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
}
}
}
}

View File

@ -11,8 +11,9 @@ import SwiftUI
class PreferencesNavigationController: UINavigationController {
init() {
let hostingController = UIHostingController(rootView: PreferencesView())
init(mastodonController: MastodonController) {
let view = PreferencesView(currentAccount: mastodonController.accountInfo!)
let hostingController = UIHostingController(rootView: view)
super.init(rootViewController: hostingController)
hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
}

View File

@ -8,6 +8,7 @@
import SwiftUI
struct PreferencesView : View {
var currentAccount: LocalData.UserAccountInfo
@State private var showingLogoutConfirmation = false
var body: some View {
@ -49,11 +50,7 @@ struct PreferencesView : View {
}
func logoutPressed() {
LocalData.shared.onboardingComplete = false
LocalData.shared.instanceURL = nil
LocalData.shared.clientID = nil
LocalData.shared.clientSecret = nil
LocalData.shared.accessToken = nil
LocalData.shared.removeAccount(currentAccount)
NotificationCenter.default.post(name: .userLoggedOut, object: nil)
}
}
@ -61,7 +58,8 @@ struct PreferencesView : View {
#if DEBUG
struct PreferencesView_Previews : PreviewProvider {
static var previews: some View {
PreferencesView()
let account = LocalData.UserAccountInfo(instanceURL: URL(string: "https://mastodon.social")!, clientID: "clientID", clientSecret: "clientSecret", username: "example", accessToken: "accessToken")
return PreferencesView(currentAccount: account)
}
}
#endif

View File

@ -50,7 +50,7 @@ class MyProfileTableViewController: ProfileTableViewController {
}
@objc func preferencesPressed() {
present(PreferencesNavigationController(), animated: true)
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
}
@objc func closePreferences() {

View File

@ -31,9 +31,8 @@ class InstanceTimelineViewController: TimelineTableViewController {
init(for url: URL) {
self.instanceURL = url
let mastodonController = MastodonController()
mastodonController.createClient(instanceURL: url)
let mastodonController = MastodonController(instanceURL: url)
super.init(for: .instance(instanceURL: url), mastodonController: mastodonController)
}

View File

@ -15,7 +15,10 @@ class UserActivityManager {
private static let encoder = PropertyListEncoder()
private static let decoder = PropertyListDecoder()
private static var mastodonController: MastodonController { .shared }
private static var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
private static func getMainTabBarController() -> MainTabBarViewController {
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!

View File

@ -13,13 +13,15 @@ import SwiftSoup
struct XCBActions {
// MARK: - Utils
private static var mastodonController: MastodonController { .shared }
private static var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene!
return scene.session.mastodonController!
}
private static func getMainTabBarController() -> MainTabBarViewController {
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first!
let window = scene.windows.first { $0.isKeyWindow }!
return window.rootViewController as! MainTabBarViewController
// return (UIApplication.shared.delegate as! AppDelegate).window!.rootViewController as! MainTabBarViewController
}
private static func show(_ vc: UIViewController) {