Move local user accounts to separate package

This commit is contained in:
Shadowfacts 2023-03-05 14:35:25 -05:00
parent 5471d810c8
commit 247bb31c56
29 changed files with 262 additions and 158 deletions

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

9
Packages/UserAccounts/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
"version" : "1.2.1"
}
},
{
"identity" : "swift-url",
"kind" : "remoteSourceControl",
"location" : "https://github.com/karwa/swift-url.git",
"state" : {
"branch" : "main",
"revision" : "220f6ab9d8a7e0742f85eb9f21b745942e001ae6"
}
}
],
"version" : 2
}

View File

@ -0,0 +1,31 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "UserAccounts",
platforms: [
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "UserAccounts",
targets: ["UserAccounts"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(path: "../Pachyderm"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "UserAccounts",
dependencies: ["Pachyderm"]),
.testTarget(
name: "UserAccountsTests",
dependencies: ["UserAccounts"]),
]
)

View File

@ -0,0 +1,3 @@
# UserAccounts
A description of this package.

View File

@ -0,0 +1,80 @@
//
// UserAccountInfo.swift
// UserAccounts
//
// Created by Shadowfacts on 3/5/23.
//
import Foundation
import CryptoKit
public struct UserAccountInfo: Equatable, Hashable {
public let id: String
public let instanceURL: URL
public let clientID: String
public let clientSecret: String
public private(set) var username: String!
public let accessToken: String
fileprivate static let tempAccountID = "temp"
static func id(instanceURL: URL, username: String?) -> String {
// We hash the instance host and username to form the account ID
// so that account IDs will match across devices, allowing for data syncing and handoff.
var hasher = SHA256()
hasher.update(data: instanceURL.host!.data(using: .utf8)!)
if let username {
hasher.update(data: username.data(using: .utf8)!)
}
return Data(hasher.finalize()).base64EncodedString()
}
/// Only to be used for temporary MastodonController needed to fetch own account info and create final UserAccountInfo with real username
public init(tempInstanceURL instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) {
self.id = UserAccountInfo.tempAccountID
self.instanceURL = instanceURL
self.clientID = clientID
self.clientSecret = clientSecret
self.accessToken = accessToken
}
init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String) {
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
self.instanceURL = instanceURL
self.clientID = clientID
self.clientSecret = clientSecret
self.username = username
self.accessToken = accessToken
}
init?(userDefaultsDict dict: [String: String]) {
guard let id = dict["id"],
let instanceURL = dict["instanceURL"],
let url = URL(string: instanceURL),
let clientID = dict["clientID"],
let secret = dict["clientSecret"],
let accessToken = dict["accessToken"] else {
return nil
}
self.id = id
self.instanceURL = url
self.clientID = clientID
self.clientSecret = secret
self.username = dict["username"]
self.accessToken = accessToken
}
/// A filename-safe string for this account
public var persistenceKey: String {
// slashes are not allowed in the persistent store coordinator name
id.replacingOccurrences(of: "/", with: "_")
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
return lhs.id == rhs.id
}
}

View File

@ -1,18 +1,16 @@
//
// LocalData.swift
// Tusker
// UserAccountsManager.swift
// UserAccounts
//
// Created by Shadowfacts on 8/18/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
// Created by Shadowfacts on 3/5/23.
//
import Foundation
import Combine
import CryptoKit
class LocalData: ObservableObject {
public class UserAccountsManager: ObservableObject {
static let shared = LocalData()
public static let shared = UserAccountsManager()
let defaults: UserDefaults
@ -38,7 +36,7 @@ class LocalData: ObservableObject {
}
private let accountsKey = "accounts"
private(set) var accounts: [UserAccountInfo] {
public private(set) var accounts: [UserAccountInfo] {
get {
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
@ -66,7 +64,7 @@ class LocalData: ObservableObject {
}
private let mostRecentAccountKey = "mostRecentAccount"
private(set) var mostRecentAccountID: String? {
public private(set) var mostRecentAccountID: String? {
get {
return defaults.string(forKey: mostRecentAccountKey)
}
@ -109,13 +107,13 @@ class LocalData: ObservableObject {
usesAccountIDHashes = true
}
// MARK: - Account Management
// MARK: Account Management
var onboardingComplete: Bool {
public var onboardingComplete: Bool {
return !accounts.isEmpty
}
func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String) -> UserAccountInfo {
public func addAccount(instanceURL url: URL, clientID: 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)
@ -126,15 +124,15 @@ class LocalData: ObservableObject {
return info
}
func removeAccount(_ info: UserAccountInfo) {
public func removeAccount(_ info: UserAccountInfo) {
accounts.removeAll(where: { $0.id == info.id })
}
func getAccount(id: String) -> UserAccountInfo? {
public func getAccount(id: String) -> UserAccountInfo? {
return accounts.first(where: { $0.id == id })
}
func getMostRecentAccount() -> UserAccountInfo? {
public func getMostRecentAccount() -> UserAccountInfo? {
guard onboardingComplete else { return nil }
let mostRecent: UserAccountInfo?
if let id = mostRecentAccountID {
@ -145,86 +143,13 @@ class LocalData: ObservableObject {
return mostRecent ?? accounts.first!
}
func setMostRecentAccount(_ account: UserAccountInfo?) {
public func setMostRecentAccount(_ account: UserAccountInfo?) {
mostRecentAccountID = account?.id
}
}
extension LocalData {
struct UserAccountInfo: Equatable, Hashable {
let id: String
let instanceURL: URL
let clientID: String
let clientSecret: String
private(set) var username: String!
let accessToken: String
fileprivate static let tempAccountID = "temp"
fileprivate static func id(instanceURL: URL, username: String?) -> String {
// We hash the instance host and username to form the account ID
// so that account IDs will match across devices, allowing for data syncing and handoff.
var hasher = SHA256()
hasher.update(data: instanceURL.host!.data(using: .utf8)!)
if let username {
hasher.update(data: username.data(using: .utf8)!)
}
return Data(hasher.finalize()).base64EncodedString()
}
/// Only to be used for temporary MastodonController needed to fetch own account info and create final UserAccountInfo with real username
init(tempInstanceURL instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) {
self.id = UserAccountInfo.tempAccountID
self.instanceURL = instanceURL
self.clientID = clientID
self.clientSecret = clientSecret
self.accessToken = accessToken
}
fileprivate init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String) {
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
self.instanceURL = instanceURL
self.clientID = clientID
self.clientSecret = clientSecret
self.username = username
self.accessToken = accessToken
}
fileprivate init?(userDefaultsDict dict: [String: String]) {
guard let id = dict["id"],
let instanceURL = dict["instanceURL"],
let url = URL(string: instanceURL),
let clientID = dict["clientID"],
let secret = dict["clientSecret"],
let accessToken = dict["accessToken"] else {
return nil
}
self.id = id
self.instanceURL = url
self.clientID = clientID
self.clientSecret = secret
self.username = dict["username"]
self.accessToken = accessToken
}
/// A filename-safe string for this account
var persistenceKey: String {
// slashes are not allowed in the persistent store coordinator name
id.replacingOccurrences(of: "/", with: "_")
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
return lhs.id == rhs.id
}
}
}
extension Notification.Name {
public extension Notification.Name {
static let userLoggedOut = Notification.Name("Tusker.userLoggedOut")
static let addAccount = Notification.Name("Tusker.addAccount")
static let activateAccount = Notification.Name("Tusker.activateAccount")

View File

@ -142,7 +142,6 @@
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; };
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; };
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
@ -268,6 +267,7 @@
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */; };
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6B0026D29B5248800C70BE2 /* UserAccounts */; };
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; };
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; };
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; };
@ -559,7 +559,6 @@
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; };
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
@ -688,6 +687,7 @@
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedImageView.swift; sourceTree = "<group>"; };
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UserAccounts; path = Packages/UserAccounts; sourceTree = "<group>"; };
D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerViewController.swift; sourceTree = "<group>"; };
D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionsListViewController.swift; sourceTree = "<group>"; };
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; };
@ -813,6 +813,7 @@
files = (
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
D659F35E2953A212002D944A /* TTTKit in Frameworks */,
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
@ -1537,6 +1538,7 @@
D674A50727F910F300BA03AC /* Pachyderm */,
D6BEA243291A0C83002F4D01 /* Duckable */,
D68A76F22953915C001DA1B3 /* TTTKit */,
D6B0026C29B5245400C70BE2 /* UserAccounts */,
D6D4DDCE212518A000E1C4BB /* Tusker */,
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
@ -1573,7 +1575,6 @@
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
D61F75BC293D099600C0B37F /* Lazy.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D61DC84528F498F200B82C6E /* Logging.swift */,
D6B81F432560390300F6E31D /* MenuController.swift */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
@ -1741,6 +1742,7 @@
D63CC701290EC0B8000E19DE /* Sentry */,
D6BEA244291A0EDE002F4D01 /* Duckable */,
D659F35D2953A212002D944A /* TTTKit */,
D6B0026D29B5248800C70BE2 /* UserAccounts */,
);
productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -2147,7 +2149,6 @@
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */,
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
@ -2966,6 +2967,10 @@
isa = XCSwiftPackageProductDependency;
productName = Pachyderm;
};
D6B0026D29B5248800C70BE2 /* UserAccounts */ = {
isa = XCSwiftPackageProductDependency;
productName = UserAccounts;
};
D6BEA244291A0EDE002F4D01 /* Duckable */ = {
isa = XCSwiftPackageProductDependency;
productName = Duckable;

View File

@ -7,13 +7,14 @@
//
import Foundation
import UserAccounts
@MainActor
class LogoutService {
let accountInfo: LocalData.UserAccountInfo
let accountInfo: UserAccountInfo
private let mastodonController: MastodonController
init(accountInfo: LocalData.UserAccountInfo) {
init(accountInfo: UserAccountInfo) {
self.accountInfo = accountInfo
self.mastodonController = MastodonController.getForAccount(accountInfo)
}
@ -23,7 +24,7 @@ class LogoutService {
try? await self.mastodonController.client.revokeAccessToken()
}
MastodonController.removeForAccount(accountInfo)
LocalData.shared.removeAccount(accountInfo)
UserAccountsManager.shared.removeAccount(accountInfo)
let psc = mastodonController.persistentContainer.persistentStoreCoordinator
for store in psc.persistentStores {
guard let url = store.url else {

View File

@ -9,15 +9,16 @@
import Foundation
import Pachyderm
import Combine
import UserAccounts
class MastodonController: ObservableObject {
static private(set) var all = [LocalData.UserAccountInfo: MastodonController]()
static private(set) var all = [UserAccountInfo: MastodonController]()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
static func getForAccount(_ account: LocalData.UserAccountInfo) -> MastodonController {
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
if let controller = all[account] {
return controller
} else {
@ -31,7 +32,7 @@ class MastodonController: ObservableObject {
}
}
static func removeForAccount(_ account: LocalData.UserAccountInfo) {
static func removeForAccount(_ account: UserAccountInfo) {
all.removeValue(forKey: account)
}
@ -43,7 +44,7 @@ class MastodonController: ObservableObject {
private(set) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL
var accountInfo: LocalData.UserAccountInfo?
var accountInfo: UserAccountInfo?
var accountPreferences: AccountPreferences!
let client: Client!

View File

@ -10,6 +10,7 @@ import UIKit
import CoreData
import OSLog
import Sentry
import UserAccounts
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
@ -32,7 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if let oldSavedData = SavedDataManager.load() {
do {
for account in oldSavedData.accountIDs {
guard let account = LocalData.shared.getAccount(id: account) else {
guard let account = UserAccountsManager.shared.getAccount(id: account) else {
continue
}
let controller = MastodonController.getForAccount(account)

View File

@ -9,11 +9,12 @@
import Foundation
import CoreData
import Pachyderm
import UserAccounts
@objc(AccountPreferences)
public final class AccountPreferences: NSManagedObject {
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<AccountPreferences> {
@nonobjc class func fetchRequest(account: UserAccountInfo) -> NSFetchRequest<AccountPreferences> {
let req = NSFetchRequest<AccountPreferences>(entityName: "AccountPreferences")
req.predicate = NSPredicate(format: "accountID = %@", account.id)
req.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
@ -27,7 +28,7 @@ public final class AccountPreferences: NSManagedObject {
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: AccountPreferences.defaultPinnedTimelines)
var pinnedTimelines: [PinnedTimeline]
static func `default`(account: LocalData.UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
static func `default`(account: UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
let prefs = AccountPreferences(context: context)
prefs.accountID = account.id
prefs.createdAt = Date()

View File

@ -13,12 +13,13 @@ import Combine
import OSLog
import Sentry
import CloudKit
import UserAccounts
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
private let accountInfo: LocalData.UserAccountInfo?
private let accountInfo: UserAccountInfo?
private static let managedObjectModel: NSManagedObjectModel = {
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
@ -51,7 +52,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
let accountSubject = PassthroughSubject<String, Never>()
let relationshipSubject = PassthroughSubject<String, Never>()
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
init(for accountInfo: UserAccountInfo?, transient: Bool = false) {
self.accountInfo = accountInfo
let group = DispatchGroup()

View File

@ -10,6 +10,7 @@ import Foundation
import CoreData
import Pachyderm
import WebURLFoundationExtras
import UserAccounts
@objc(SavedHashtag)
public final class SavedHashtag: NSManagedObject {
@ -18,13 +19,13 @@ public final class SavedHashtag: NSManagedObject {
return NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
}
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
@nonobjc class func fetchRequest(account: UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
req.predicate = NSPredicate(format: "accountID = %@", account.id)
return req
}
@nonobjc class func fetchRequest(name: String, account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
@nonobjc class func fetchRequest(name: String, account: UserAccountInfo) -> NSFetchRequest<SavedHashtag> {
let req = NSFetchRequest<SavedHashtag>(entityName: "SavedHashtag")
req.predicate = NSPredicate(format: "name LIKE[cd] %@ AND accountID = %@", name, account.id)
return req
@ -37,7 +38,7 @@ public final class SavedHashtag: NSManagedObject {
}
extension SavedHashtag {
convenience init(hashtag: Hashtag, account: LocalData.UserAccountInfo, context: NSManagedObjectContext) {
convenience init(hashtag: Hashtag, account: UserAccountInfo, context: NSManagedObjectContext) {
self.init(context: context)
self.accountID = account.id
self.name = hashtag.name

View File

@ -8,6 +8,7 @@
import Foundation
import CoreData
import UserAccounts
@objc(SavedInstance)
public final class SavedInstance: NSManagedObject {
@ -16,13 +17,13 @@ public final class SavedInstance: NSManagedObject {
return NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
}
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedInstance> {
@nonobjc class func fetchRequest(account: UserAccountInfo) -> NSFetchRequest<SavedInstance> {
let req = NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
req.predicate = NSPredicate(format: "accountID = %@", account.id)
return req
}
@nonobjc class func fetchRequest(url: URL, account: LocalData.UserAccountInfo) -> NSFetchRequest<SavedInstance> {
@nonobjc class func fetchRequest(url: URL, account: UserAccountInfo) -> NSFetchRequest<SavedInstance> {
let req = NSFetchRequest<SavedInstance>(entityName: "SavedInstance")
req.predicate = NSPredicate(format: "url = %@ AND accountID = %@", url as NSURL, account.id)
return req
@ -34,7 +35,7 @@ public final class SavedInstance: NSManagedObject {
}
extension SavedInstance {
convenience init(url: URL, account: LocalData.UserAccountInfo, context: NSManagedObjectContext) {
convenience init(url: URL, account: UserAccountInfo, context: NSManagedObjectContext) {
self.init(context: context)
self.accountID = account.id
self.url = url

View File

@ -9,11 +9,12 @@
import Foundation
import CoreData
import Pachyderm
import UserAccounts
@objc(TimelinePosition)
public final class TimelinePosition: NSManagedObject {
@nonobjc class func fetchRequest(timeline: Timeline, account: LocalData.UserAccountInfo) -> NSFetchRequest<TimelinePosition> {
@nonobjc class func fetchRequest(timeline: Timeline, account: UserAccountInfo) -> NSFetchRequest<TimelinePosition> {
let req = NSFetchRequest<TimelinePosition>(entityName: "TimelinePosition")
req.predicate = NSPredicate(format: "accountID = %@ AND timelineKind = %@", account.id, toTimelineKind(timeline))
req.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
@ -34,7 +35,7 @@ public final class TimelinePosition: NSManagedObject {
set { timelineKind = toTimelineKind(newValue) }
}
convenience init(timeline: Timeline, account: LocalData.UserAccountInfo, context: NSManagedObjectContext) {
convenience init(timeline: Timeline, account: UserAccountInfo, context: NSManagedObjectContext) {
self.init(context: context)
self.timeline = timeline
self.accountID = account.id

View File

@ -8,6 +8,7 @@
import UIKit
import Pachyderm
import UserAccounts
class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
@ -31,11 +32,11 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
}
launchActivity = activity
let account: LocalData.UserAccountInfo
let account: UserAccountInfo
if let activityAccount = UserActivityManager.getAccount(from: activity) {
account = activityAccount
} else if let mostRecent = LocalData.shared.getMostRecentAccount() {
} else if let mostRecent = UserAccountsManager.shared.getMostRecentAccount() {
account = mostRecent
} else {
// without an account, we can't do anything so we just destroy the scene

View File

@ -8,6 +8,7 @@
import UIKit
import Combine
import UserAccounts
class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
@ -22,12 +23,12 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
return
}
guard LocalData.shared.onboardingComplete else {
guard UserAccountsManager.shared.onboardingComplete else {
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
return
}
let account: LocalData.UserAccountInfo
let account: UserAccountInfo
let controller: MastodonController
let draft: Draft?
@ -36,7 +37,7 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
account = activityAccount
} else {
// todo: this potentially changes the account for the draft, should show the same warning to user as in the drafts selection screen
account = LocalData.shared.getMostRecentAccount()!
account = UserAccountsManager.shared.getMostRecentAccount()!
}
controller = MastodonController.getForAccount(account)
@ -49,7 +50,7 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
draft = nil
}
} else {
account = LocalData.shared.getMostRecentAccount()!
account = UserAccountsManager.shared.getMostRecentAccount()!
controller = MastodonController.getForAccount(account)
draft = nil
}

View File

@ -11,6 +11,7 @@ import Pachyderm
import MessageUI
import CoreData
import Duckable
import UserAccounts
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
@ -161,13 +162,13 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
let session = session ?? window!.windowScene!.session
if LocalData.shared.onboardingComplete {
let account: LocalData.UserAccountInfo
if UserAccountsManager.shared.onboardingComplete {
let account: UserAccountInfo
if let activity = launchActivity,
let activityAccount = UserActivityManager.getAccount(from: activity) {
account = activityAccount
} else {
account = LocalData.shared.getMostRecentAccount()!
account = UserAccountsManager.shared.getMostRecentAccount()!
}
if session.mastodonController == nil {
@ -194,9 +195,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
}
}
func activateAccount(_ account: LocalData.UserAccountInfo, animated: Bool) {
let oldMostRecentAccount = LocalData.shared.mostRecentAccountID
LocalData.shared.setMostRecentAccount(account)
func activateAccount(_ account: UserAccountInfo, animated: Bool) {
let oldMostRecentAccount = UserAccountsManager.shared.mostRecentAccountID
UserAccountsManager.shared.setMostRecentAccount(account)
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
// iPadOS shows the title below the App Name
@ -212,8 +213,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
let direction: AccountSwitchingContainerViewController.AnimationDirection
if animated,
let oldIndex = LocalData.shared.accounts.firstIndex(where: { $0.id == oldMostRecentAccount }),
let newIndex = LocalData.shared.accounts.firstIndex(of: account) {
let oldIndex = UserAccountsManager.shared.accounts.firstIndex(where: { $0.id == oldMostRecentAccount }),
let newIndex = UserAccountsManager.shared.accounts.firstIndex(of: account) {
direction = newIndex > oldIndex ? .upwards : .downwards
} else {
direction = .none
@ -229,8 +230,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
return
}
LogoutService(accountInfo: account).run()
if LocalData.shared.onboardingComplete {
activateAccount(LocalData.shared.accounts.first!, animated: false)
if UserAccountsManager.shared.onboardingComplete {
activateAccount(UserAccountsManager.shared.accounts.first!, animated: false)
} else {
window!.rootViewController = createOnboardingUI()
}
@ -269,7 +270,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
}
extension MainSceneDelegate: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
func didFinishOnboarding(account: UserAccountInfo) {
activateAccount(account, animated: false)
}
}

View File

@ -7,6 +7,7 @@
//
import UIKit
import UserAccounts
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
@ -139,9 +140,9 @@ class FastAccountSwitcherViewController: UIViewController {
addAccountPlaceholder
]
for account in LocalData.shared.accounts {
for account in UserAccountsManager.shared.accounts {
let accountView = FastSwitchingAccountView(account: account, orientation: itemOrientation)
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
accountView.isCurrent = account.id == UserAccountsManager.shared.mostRecentAccountID
accountsStack.addArrangedSubview(accountView)
accountViews.append(accountView)
}
@ -168,9 +169,9 @@ class FastAccountSwitcherViewController: UIViewController {
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAddAccount()
}
} else {
let account = LocalData.shared.accounts[newIndex - 1]
let account = UserAccountsManager.shared.accounts[newIndex - 1]
if account.id != LocalData.shared.mostRecentAccountID {
if account.id != UserAccountsManager.shared.mostRecentAccountID {
if hapticFeedback {
selectionChangedFeedbackGenerator?.selectionChanged()
}

View File

@ -7,6 +7,7 @@
//
import UIKit
import UserAccounts
class FastSwitchingAccountView: UIView {
@ -49,7 +50,7 @@ class FastSwitchingAccountView: UIView {
private var avatarRequest: ImageCache.Request?
init(account: LocalData.UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
init(account: UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
self.orientation = orientation
super.init(frame: .zero)
commonInit()
@ -121,7 +122,7 @@ class FastSwitchingAccountView: UIView {
isAccessibilityElement = true
}
private func setupAccount(account: LocalData.UserAccountInfo) {
private func setupAccount(account: UserAccountInfo) {
usernameLabel.text = account.username
instanceLabel.text = account.instanceURL.host!
let controller = MastodonController.getForAccount(account)

View File

@ -8,6 +8,7 @@
import UIKit
import ScreenCorners
import UserAccounts
class AccountSwitchingContainerViewController: UIViewController {
@ -16,7 +17,7 @@ class AccountSwitchingContainerViewController: UIViewController {
private var userActivities: [String: NSUserActivity] = [:]
init(root: TuskerRootViewController, for account: LocalData.UserAccountInfo) {
init(root: TuskerRootViewController, for account: UserAccountInfo) {
self.currentAccountID = account.id
self.root = root
@ -33,7 +34,7 @@ class AccountSwitchingContainerViewController: UIViewController {
embedChild(root)
}
func setRoot(_ newRoot: TuskerRootViewController, for account: LocalData.UserAccountInfo, animating direction: AnimationDirection) {
func setRoot(_ newRoot: TuskerRootViewController, for account: UserAccountInfo, animating direction: AnimationDirection) {
let oldRoot = self.root
if direction == .none {
oldRoot.removeViewAndController()

View File

@ -7,6 +7,7 @@
//
import UIKit
import UserAccounts
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
@ -35,7 +36,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
fatalError("init(coder:) has not been implemented")
}
func updateUI(item: MainSidebarViewController.Item, account: LocalData.UserAccountInfo) async {
func updateUI(item: MainSidebarViewController.Item, account: UserAccountInfo) async {
var config = defaultContentConfiguration()
config.text = item.title
config.image = UIImage(systemName: item.imageName!)

View File

@ -10,10 +10,11 @@ import UIKit
import AuthenticationServices
import Pachyderm
import OSLog
import UserAccounts
protocol OnboardingViewControllerDelegate {
@MainActor
func didFinishOnboarding(account: LocalData.UserAccountInfo)
func didFinishOnboarding(account: UserAccountInfo)
}
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "OnboardingViewController")
@ -145,7 +146,7 @@ class OnboardingViewController: UINavigationController {
}
// construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
let tempAccountInfo = LocalData.UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
let tempAccountInfo = UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
mastodonController.accountInfo = tempAccountInfo
updateStatus("Checking Credentials")
@ -158,7 +159,7 @@ class OnboardingViewController: UINavigationController {
throw Error.gettingOwnAccount(error)
}
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
mastodonController.accountInfo = accountInfo
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)

View File

@ -9,6 +9,7 @@ import SwiftUI
import Pachyderm
import CoreData
import CloudKit
import UserAccounts
struct AdvancedPrefsView : View {
@ObservedObject var preferences = Preferences.shared
@ -30,7 +31,7 @@ struct AdvancedPrefsView : View {
var formattingFooter: some View {
var s: AttributedString = "This option is only supported with Pleroma and some compatible Mastodon instances (such as Glitch).\n"
if let account = LocalData.shared.getMostRecentAccount() {
if let account = UserAccountsManager.shared.getMostRecentAccount() {
let mastodonController = MastodonController.getForAccount(account)
// shouldn't need to load the instance here, because loading it is kicked off my the scene delegate
if !mastodonController.instanceFeatures.probablySupportsMarkdown {
@ -135,7 +136,7 @@ struct AdvancedPrefsView : View {
].map {
$0.getDiskSizeInBytes() ?? 0
}.reduce(0, +)
mastodonCacheSize = LocalData.shared.accounts.map {
mastodonCacheSize = UserAccountsManager.shared.accounts.map {
let descriptions = MastodonController.getForAccount($0).persistentContainer.persistentStoreDescriptions
return descriptions.map {
guard let url = $0.url else {
@ -148,7 +149,7 @@ struct AdvancedPrefsView : View {
}
private func clearCache() {
for account in LocalData.shared.accounts {
for account in UserAccountsManager.shared.accounts {
let controller = MastodonController.getForAccount(account)
let container = controller.persistentContainer
do {
@ -178,7 +179,7 @@ struct AdvancedPrefsView : View {
}
private func resetUI() {
let mostRecent = LocalData.shared.getMostRecentAccount()!
let mostRecent = UserAccountsManager.shared.getMostRecentAccount()!
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent])
}
}

View File

@ -7,9 +7,10 @@
//
import SwiftUI
import UserAccounts
struct LocalAccountAvatarView: View {
let localAccountInfo: LocalData.UserAccountInfo
let localAccountInfo: UserAccountInfo
@State var avatarImage: UIImage? = nil
@ObservedObject var preferences = Preferences.shared

View File

@ -8,6 +8,7 @@
import UIKit
import SwiftUI
import UserAccounts
class PreferencesNavigationController: UINavigationController {
@ -64,7 +65,7 @@ class PreferencesNavigationController: UINavigationController {
guard let windowScene = self.view.window?.windowScene else {
return
}
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
let account = notification.userInfo!["account"] as! UserAccountInfo
if let sceneDelegate = windowScene.delegate as? MainSceneDelegate {
isSwitchingAccounts = true
dismiss(animated: true) { // dismiss preferences
@ -85,8 +86,8 @@ class PreferencesNavigationController: UINavigationController {
sceneDelegate.logoutCurrent()
}
} else {
LogoutService(accountInfo: LocalData.shared.getMostRecentAccount()!).run()
let accountID = LocalData.shared.getMostRecentAccount()?.id
LogoutService(accountInfo: UserAccountsManager.shared.getMostRecentAccount()!).run()
let accountID = UserAccountsManager.shared.getMostRecentAccount()?.id
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: UserActivityManager.mainSceneActivity(accountID: accountID), options: nil)
UIApplication.shared.requestSceneSessionDestruction(windowScene.session, options: nil)
}
@ -95,7 +96,7 @@ class PreferencesNavigationController: UINavigationController {
}
extension PreferencesNavigationController: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) {
func didFinishOnboarding(account: UserAccountInfo) {
guard let windowScene = self.view.window?.windowScene else {
return
}

View File

@ -6,11 +6,12 @@
//
import SwiftUI
import UserAccounts
struct PreferencesView: View {
let mastodonController: MastodonController
@ObservedObject private var localData = LocalData.shared
@ObservedObject private var userAccounts = UserAccountsManager.shared
@State private var showingLogoutConfirmation = false
init(mastodonController: MastodonController) {
@ -31,7 +32,7 @@ struct PreferencesView: View {
private var accountsSection: some View {
Section {
ForEach(localData.accounts, id: \.accessToken) { (account) in
ForEach(userAccounts.accounts, id: \.accessToken) { (account) in
Button(action: {
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
}) {
@ -58,12 +59,12 @@ struct PreferencesView: View {
}.onDelete { (indices: IndexSet) in
var indices = indices
var logoutFromCurrent = false
if let index = indices.first(where: { localData.accounts[$0] == mastodonController.accountInfo! }) {
if let index = indices.first(where: { userAccounts.accounts[$0] == mastodonController.accountInfo! }) {
logoutFromCurrent = true
indices.remove(index)
}
indices.forEach { LogoutService(accountInfo: localData.accounts[$0]).run() }
indices.forEach { LogoutService(accountInfo: userAccounts.accounts[$0]).run() }
if logoutFromCurrent {
self.logoutPressed()

View File

@ -10,6 +10,7 @@ import UIKit
import Intents
import Pachyderm
import OSLog
import UserAccounts
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
@ -32,11 +33,11 @@ class UserActivityManager {
scene.session.mastodonController!
}
static func getAccount(from activity: NSUserActivity) -> LocalData.UserAccountInfo? {
static func getAccount(from activity: NSUserActivity) -> UserAccountInfo? {
guard let id = activity.userInfo?["accountID"] as? String else {
return nil
}
return LocalData.shared.getAccount(id: id)
return UserAccountsManager.shared.getAccount(id: id)
}
// MARK: - Main Scene