Compare commits

..

No commits in common. "c65b69cfbd3f1275ee43299b8e77fc1641226abb" and "55e4966bd1aa609860d485ac3cd3e29b8270f526" have entirely different histories.

17 changed files with 132 additions and 337 deletions

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct ClientRegistration: Decodable, Sendable { public struct ClientRegistration: Decodable {
public let clientID: String public let clientID: String
public let clientSecret: String public let clientSecret: String

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation import Foundation
public struct Feed: Decodable, Sendable { public struct Feed: Decodable {
public let id: FervorID public let id: FervorID
public let title: String public let title: String
public let url: URL? public let url: URL?

View File

@ -5,13 +5,13 @@
// Created by Shadowfacts on 11/25/21. // Created by Shadowfacts on 11/25/21.
// //
@preconcurrency import Foundation import Foundation
public actor FervorClient: Sendable { public class FervorClient {
private let instanceURL: URL let instanceURL: URL
private let session: URLSession let session: URLSession
public private(set) var accessToken: String? public var accessToken: String?
private let decoder: JSONDecoder = { private let decoder: JSONDecoder = {
let d = JSONDecoder() let d = JSONDecoder()
@ -81,9 +81,7 @@ public actor FervorClient: Sendable {
"client_id": clientID, "client_id": clientID,
"client_secret": clientSecret, "client_secret": clientSecret,
]) ])
let result: Token = try await performRequest(request) return try await performRequest(request)
self.accessToken = result.accessToken
return result
} }
public func groups() async throws -> [Group] { public func groups() async throws -> [Group] {

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation import Foundation
public struct Group: Decodable, Sendable { public struct Group: Decodable {
public let id: FervorID public let id: FervorID
public let title: String public let title: String
public let feedIDs: [FervorID] public let feedIDs: [FervorID]

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation import Foundation
public struct Instance: Decodable, Sendable { public struct Instance: Decodable {
public let name: String public let name: String
public let url: URL public let url: URL
public let version: String public let version: String

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation import Foundation
public struct Item: Decodable, Sendable { public struct Item: Decodable {
public let id: FervorID public let id: FervorID
public let feedID: FervorID public let feedID: FervorID
public let title: String? public let title: String?

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 1/9/22. // Created by Shadowfacts on 1/9/22.
// //
@preconcurrency import Foundation import Foundation
public struct ItemsSyncUpdate: Decodable, Sendable { public struct ItemsSyncUpdate: Decodable {
public let syncTimestamp: Date public let syncTimestamp: Date
public let delete: [FervorID] public let delete: [FervorID]

View File

@ -7,11 +7,10 @@
import Foundation import Foundation
public struct Token: Codable, Sendable { public struct Token: Decodable {
public let accessToken: String public let accessToken: String
public let expiresIn: Int? public let expiresIn: Int?
public let refreshToken: String? public let refreshToken: String?
public let owner: String?
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
@ -19,21 +18,11 @@ public struct Token: Codable, Sendable {
self.accessToken = try container.decode(String.self, forKey: .accessToken) self.accessToken = try container.decode(String.self, forKey: .accessToken)
self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn)
self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken)
self.owner = try container.decodeIfPresent(String.self, forKey: .owner)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(accessToken, forKey: .accessToken)
try container.encodeIfPresent(expiresIn, forKey: .expiresIn)
try container.encodeIfPresent(refreshToken, forKey: .refreshToken)
try container.encodeIfPresent(owner, forKey: .owner)
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case accessToken = "access_token" case accessToken = "access_token"
case expiresIn = "expires_in" case expiresIn = "expires_in"
case refreshToken = "refresh_token" case refreshToken = "refresh_token"
case owner
} }
} }

View File

@ -9,8 +9,7 @@ import CoreData
import Fervor import Fervor
import OSLog import OSLog
// todo: is this actually sendable? class PersistentContainer: NSPersistentContainer {
class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
private static let managedObjectModel: NSManagedObjectModel = { private static let managedObjectModel: NSManagedObjectModel = {
let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")! let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")!
@ -24,14 +23,14 @@ class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
return context return context
}() }()
weak var fervorController: FervorController? private weak var fervorController: FervorController?
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
init(account: LocalData.Account) { init(account: LocalData.Account, fervorController: FervorController) {
// slashes the base64 string turn into subdirectories which we don't want self.fervorController = fervorController
let name = account.id.base64EncodedString().replacingOccurrences(of: "/", with: "_")
super.init(name: name, managedObjectModel: PersistentContainer.managedObjectModel) super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel)
loadPersistentStores { description, error in loadPersistentStores { description, error in
if let error = error { if let error = error {
@ -41,7 +40,7 @@ class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
} }
@MainActor @MainActor
func saveViewContext() throws { private func saveViewContext() async throws {
if viewContext.hasChanges { if viewContext.hasChanges {
try viewContext.save() try viewContext.save()
} }

View File

@ -5,12 +5,12 @@
// Created by Shadowfacts on 11/25/21. // Created by Shadowfacts on 11/25/21.
// //
@preconcurrency import Foundation import Foundation
import Fervor import Fervor
@preconcurrency import OSLog import OSLog
import Combine import Combine
actor FervorController { class FervorController {
static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")! static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")!
@ -19,40 +19,36 @@ actor FervorController {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
let client: FervorClient let client: FervorClient
nonisolated let account: LocalData.Account? private(set) var account: LocalData.Account?
private(set) var clientID: String? private(set) var clientID: String?
private(set) var clientSecret: String? private(set) var clientSecret: String?
private(set) var token: Token? private(set) var accessToken: String?
nonisolated let persistentContainer: PersistentContainer! private(set) var persistentContainer: PersistentContainer!
nonisolated let syncState = PassthroughSubject<SyncState, Never>() @Published private(set) var syncState = SyncState.done
private var lastSyncState = SyncState.done
private var cancellables = Set<AnyCancellable>()
init(instanceURL: URL, account: LocalData.Account?) async { init(instanceURL: URL) {
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken) self.client = FervorClient(instanceURL: instanceURL, accessToken: nil)
self.account = account
self.clientID = account?.clientID
self.clientSecret = account?.clientSecret
if let account = account {
self.persistentContainer = PersistentContainer(account: account)
} else {
self.persistentContainer = nil
}
persistentContainer?.fervorController = self
} }
convenience init(account: LocalData.Account) async { convenience init(account: LocalData.Account) {
await self.init(instanceURL: account.instanceURL, account: account) self.init(instanceURL: account.instanceURL)
self.account = account
self.clientID = account.clientID
self.clientSecret = account.clientSecret
self.accessToken = account.accessToken
self.client.accessToken = account.accessToken
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
} }
private func setSyncState(_ state: SyncState) { private func setSyncState(_ state: SyncState) {
lastSyncState = state DispatchQueue.main.async {
syncState.send(state) self.syncState = state
}
} }
func register() async throws -> ClientRegistration { func register() async throws -> ClientRegistration {
@ -63,11 +59,13 @@ actor FervorController {
} }
func getToken(authCode: String) async throws { func getToken(authCode: String) async throws {
token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
client.accessToken = token.accessToken
accessToken = token.accessToken
} }
func syncAll() async throws { func syncAll() async throws {
guard lastSyncState == .done else { guard syncState == .done else {
return return
} }
// always return to .done, even if we throw and stop syncing early // always return to .done, even if we throw and stop syncing early
@ -140,7 +138,7 @@ actor FervorController {
func markItem(_ item: Item, read: Bool) async { func markItem(_ item: Item, read: Bool) async {
item.read = read item.read = read
do { do {
let f = read ? client.read(item:) : client.unread(item:) let f = item.read ? client.read(item:) : client.unread(item:)
_ = try await f(item.id!) _ = try await f(item.id!)
item.needsReadStateSync = false item.needsReadStateSync = false
} catch { } catch {
@ -148,10 +146,12 @@ actor FervorController {
item.needsReadStateSync = true item.needsReadStateSync = true
} }
do { if persistentContainer.viewContext.hasChanges {
try self.persistentContainer.saveViewContext() do {
} catch { try persistentContainer.viewContext.save()
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)") } catch {
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
}
} }
} }

View File

@ -4,10 +4,6 @@
<dict> <dict>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-unread</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-all</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-feed</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-group</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.preferences</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.preferences</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.add-account</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.add-account</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.activate-account</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.activate-account</string>

View File

@ -6,8 +6,6 @@
// //
import Foundation import Foundation
import Fervor
import CryptoKit
struct LocalData { struct LocalData {
@ -30,12 +28,15 @@ struct LocalData {
} }
} }
static var mostRecentAccountID: Data? { static var mostRecentAccountID: UUID? {
get { get {
return UserDefaults.standard.data(forKey: "mostRecentAccountID") guard let str = UserDefaults.standard.string(forKey: "mostRecentAccountID") else {
return nil
}
return UUID(uuidString: str)
} }
set { set {
UserDefaults.standard.set(newValue, forKey: "mostRecentAccountID") UserDefaults.standard.set(newValue?.uuidString, forKey: "mostRecentAccountID")
} }
} }
@ -43,32 +44,23 @@ struct LocalData {
guard let id = mostRecentAccountID else { guard let id = mostRecentAccountID else {
return nil return nil
} }
return account(with: id)
}
static func account(with id: Data) -> Account? {
return accounts.first(where: { $0.id == id }) return accounts.first(where: { $0.id == id })
} }
struct Account: Codable { struct Account: Codable {
let id: Data let id: UUID
let instanceURL: URL let instanceURL: URL
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
let token: Token let accessToken: String
// todo: refresh tokens
init(instanceURL: URL, clientID: String, clientSecret: String, token: Token) {
// we use a hash of instance host and account id rather than random ids so that
// user activites can uniquely identify accounts across devices
var hasher = SHA256()
hasher.update(data: instanceURL.host!.data(using: .utf8)!)
hasher.update(data: token.owner!.data(using: .utf8)!)
self.id = Data(hasher.finalize())
init(instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) {
self.id = UUID()
self.instanceURL = instanceURL self.instanceURL = instanceURL
self.clientID = clientID self.clientID = clientID
self.clientSecret = clientSecret self.clientSecret = clientSecret
self.token = token self.accessToken = accessToken
} }
} }

View File

@ -5,7 +5,6 @@
// Created by Shadowfacts on 10/29/21. // Created by Shadowfacts on 10/29/21.
// //
@preconcurrency import Foundation
import UIKit import UIKit
import OSLog import OSLog
@ -27,32 +26,22 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window = UIWindow(windowScene: windowScene) window = UIWindow(windowScene: windowScene)
window!.tintColor = .appTintColor window!.tintColor = .appTintColor
var activity = connectionOptions.userActivities.first let activity = connectionOptions.userActivities.first
var account = LocalData.mostRecentAccount()
if activity?.activityType == NSUserActivity.addAccountType { if activity?.activityType == NSUserActivity.addAccountType {
account = nil let loginVC = LoginViewController()
} else if let id = activity?.accountID() { loginVC.delegate = self
account = LocalData.account(with: id) window!.rootViewController = loginVC
if account == nil { } else if activity?.activityType == NSUserActivity.activateAccountType,
activity = nil let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) {
logger.log("Missing account for activity, not restoring") fervorController = FervorController(account: account)
} createAppUI()
} } else if let account = LocalData.mostRecentAccount() {
fervorController = FervorController(account: account)
if let account = account { createAppUI()
Task { @MainActor [activity] in
fervorController = await FervorController(account: account)
syncFromServer()
createAppUI()
if let activity = activity {
setupUI(from: activity)
}
setupSceneActivationConditions()
}
} else { } else {
createLoginUI() let loginVC = LoginViewController()
loginVC.delegate = self
window!.rootViewController = loginVC
} }
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
@ -108,64 +97,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// to restore the scene back to its current state. // to restore the scene back to its current state.
} }
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
setupUI(from: userActivity)
}
private func setupUI(from activity: NSUserActivity) {
guard let split = window?.rootViewController as? AppSplitViewController else {
logger.error("Failed to setup UI for user activity: missing split VC")
return
}
switch activity.activityType {
case NSUserActivity.readUnreadType:
split.selectHomeItem(.unread)
case NSUserActivity.readAllType:
split.selectHomeItem(.all)
case NSUserActivity.readFeedType:
guard let feedID = activity.feedID() else {
break
}
let req = Feed.fetchRequest()
req.predicate = NSPredicate(format: "id = %@", feedID)
if let feed = try? fervorController.persistentContainer.viewContext.fetch(req).first {
split.selectHomeItem(.feed(feed))
}
case NSUserActivity.readGroupType:
guard let groupID = activity.groupID() else {
break
}
let req = Group.fetchRequest()
req.predicate = NSPredicate(format: "id = %@", groupID)
if let group = try? fervorController.persistentContainer.viewContext.fetch(req).first {
split.selectHomeItem(.group(group))
}
default:
break
}
}
private func createLoginUI() {
let vc = LoginViewController()
vc.delegate = self
window!.rootViewController = vc
}
private func createAppUI() { private func createAppUI() {
window!.rootViewController = AppSplitViewController(fervorController: fervorController) window!.rootViewController = AppSplitViewController(fervorController: fervorController)
} }
private func setupSceneActivationConditions() {
guard let account = fervorController?.account else {
return
}
let scene = self.window!.windowScene!
// todo: why the fuck doesn't this work
// it always picks the most recently focused window
scene.activationConditions.prefersToActivateForTargetContentIdentifierPredicate = NSPredicate(format: "self == '\(account.id.base64EncodedString())'")
scene.activationConditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(value: false)
}
private func syncFromServer() { private func syncFromServer() {
guard let fervorController = fervorController else { guard let fervorController = fervorController else {
return return
@ -190,9 +125,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
} }
func switchToAccount(_ account: LocalData.Account) async { func switchToAccount(_ account: LocalData.Account) {
LocalData.mostRecentAccountID = account.id LocalData.mostRecentAccountID = account.id
fervorController = await FervorController(account: account) fervorController = FervorController(account: account)
createAppUI() createAppUI()
syncFromServer() syncFromServer()
} }
@ -201,18 +136,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
extension SceneDelegate: LoginViewControllerDelegate { extension SceneDelegate: LoginViewControllerDelegate {
func didLogin(with controller: FervorController) { func didLogin(with controller: FervorController) {
Task { @MainActor in let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!)
let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, token: await controller.token!) LocalData.accounts.append(account)
LocalData.accounts.append(account) LocalData.mostRecentAccountID = account.id
LocalData.mostRecentAccountID = account.id fervorController = FervorController(account: account)
fervorController = await FervorController(account: account)
createAppUI() createAppUI()
syncFromServer() syncFromServer()
setupSceneActivationConditions()
UIMenuSystem.main.setNeedsRebuild() UIMenuSystem.main.setNeedsRebuild()
}
} }
} }

View File

@ -54,18 +54,6 @@ class AppSplitViewController: UISplitViewController {
setViewController(nav, for: .compact) setViewController(nav, for: .compact)
} }
func selectHomeItem(_ item: HomeViewController.Item) {
let column: Column
if traitCollection.horizontalSizeClass == .compact {
column = .compact
} else {
column = .primary
}
let nav = viewController(for: column) as! UINavigationController
let home = nav.viewControllers.first! as! HomeViewController
home.selectItem(item)
}
} }
extension AppSplitViewController: ItemsViewControllerDelegate { extension AppSplitViewController: ItemsViewControllerDelegate {
@ -77,9 +65,7 @@ extension AppSplitViewController: ItemsViewControllerDelegate {
extension AppSplitViewController: HomeViewControllerDelegate { extension AppSplitViewController: HomeViewControllerDelegate {
func switchToAccount(_ account: LocalData.Account) { func switchToAccount(_ account: LocalData.Account) {
if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { if let delegate = view.window?.windowScene?.delegate as? SceneDelegate {
Task { @MainActor in delegate.switchToAccount(account)
await delegate.switchToAccount(account)
}
} }
} }
} }

View File

@ -93,7 +93,7 @@ class HomeViewController: UIViewController {
feedResultsController.delegate = self feedResultsController.delegate = self
try! feedResultsController.performFetch() try! feedResultsController.performFetch()
fervorController.syncState fervorController.$syncState
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil) .debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
.sink { [unowned self] in .sink { [unowned self] in
self.syncStateChanged($0) self.syncStateChanged($0)
@ -149,16 +149,8 @@ class HomeViewController: UIViewController {
} }
private func syncStateChanged(_ newState: FervorController.SyncState) { private func syncStateChanged(_ newState: FervorController.SyncState) {
if newState == .done { if newState == .done && syncStateView == nil {
// update unread counts for visible items return
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems(snapshot.itemIdentifiers)
dataSource.apply(snapshot, animatingDifferences: false)
if syncStateView == nil {
// no sync state view, nothing further to update
return
}
} }
func updateView(_ syncStateView: SyncStateView) { func updateView(_ syncStateView: SyncStateView) {
@ -206,28 +198,6 @@ class HomeViewController: UIViewController {
} }
} }
private func itemsViewController(for item: Item) -> ItemsViewController {
let vc = ItemsViewController(fetchRequest: item.idFetchRequest, fervorController: fervorController)
vc.title = item.title
vc.delegate = itemsDelegate
switch item {
case .all:
vc.userActivity = .readAll(account: fervorController.account!)
case .unread:
vc.userActivity = .readUnread(account: fervorController.account!)
case .group(let group):
vc.userActivity = .readGroup(group, account: fervorController.account!)
case .feed(let feed):
vc.userActivity = .readFeed(feed, account: fervorController.account!)
}
return vc
}
func selectItem(_ item: Item) {
navigationController!.popToRootViewController(animated: false)
navigationController!.pushViewController(itemsViewController(for: item), animated: false)
}
} }
extension HomeViewController { extension HomeViewController {
@ -266,6 +236,21 @@ extension HomeViewController {
} }
} }
var fetchRequest: NSFetchRequest<Reader.Item> {
let req = Reader.Item.fetchRequest()
switch self {
case .unread:
req.predicate = NSPredicate(format: "read = NO")
case .all:
break
case .group(let group):
req.predicate = NSPredicate(format: "feed in %@", group.feeds!)
case .feed(let feed):
req.predicate = NSPredicate(format: "feed = %@", feed)
}
return req
}
var idFetchRequest: NSFetchRequest<NSManagedObjectID> { var idFetchRequest: NSFetchRequest<NSManagedObjectID> {
let req = NSFetchRequest<NSManagedObjectID>(entityName: "Item") let req = NSFetchRequest<NSManagedObjectID>(entityName: "Item")
req.resultType = .managedObjectIDResultType req.resultType = .managedObjectIDResultType
@ -322,7 +307,10 @@ extension HomeViewController: UICollectionViewDelegate {
guard let item = dataSource.itemIdentifier(for: indexPath) else { guard let item = dataSource.itemIdentifier(for: indexPath) else {
return return
} }
show(itemsViewController(for: item), sender: nil) let vc = ItemsViewController(fetchRequest: item.idFetchRequest, fervorController: fervorController)
vc.title = item.title
vc.delegate = itemsDelegate
show(vc, sender: nil)
UISelectionFeedbackGenerator().selectionChanged() UISelectionFeedbackGenerator().selectionChanged()
} }
@ -330,8 +318,8 @@ extension HomeViewController: UICollectionViewDelegate {
guard let item = dataSource.itemIdentifier(for: indexPath) else { guard let item = dataSource.itemIdentifier(for: indexPath) else {
return nil return nil
} }
return UIContextMenuConfiguration(identifier: nil, previewProvider: { [unowned self] in return UIContextMenuConfiguration(identifier: nil, previewProvider: {
return self.itemsViewController(for: item) return ItemsViewController(fetchRequest: item.idFetchRequest, fervorController: self.fervorController)
}, actionProvider: nil) }, actionProvider: nil)
} }

View File

@ -5,7 +5,6 @@
// Created by Shadowfacts on 11/25/21. // Created by Shadowfacts on 11/25/21.
// //
@preconcurrency import Foundation
import UIKit import UIKit
import AuthenticationServices import AuthenticationServices
import Fervor import Fervor
@ -73,7 +72,7 @@ class LoginViewController: UIViewController {
textField.isEnabled = false textField.isEnabled = false
activityIndicator.startAnimating() activityIndicator.startAnimating()
let controller = await FervorController(instanceURL: components.url!, account: nil) let controller = FervorController(instanceURL: components.url!)
let registration: ClientRegistration let registration: ClientRegistration
do { do {
@ -100,16 +99,16 @@ class LoginViewController: UIViewController {
let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false) let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false)
guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }), guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }),
let codeValue = codeItem.value else { let codeValue = codeItem.value else {
Task { @MainActor in DispatchQueue.main.async {
let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert) let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true) self.present(alert, animated: true)
self.textField.isEnabled = true self.textField.isEnabled = true
self.activityIndicator.stopAnimating() self.activityIndicator.stopAnimating()
} }
return return
} }
Task { @MainActor in Task { @MainActor in
do { do {
try await controller.getToken(authCode: codeValue) try await controller.getToken(authCode: codeValue)

View File

@ -12,40 +12,6 @@ extension NSUserActivity {
static let preferencesType = "net.shadowfacts.Reader.activity.preferences" static let preferencesType = "net.shadowfacts.Reader.activity.preferences"
static let addAccountType = "net.shadowfacts.Reader.activity.add-account" static let addAccountType = "net.shadowfacts.Reader.activity.add-account"
static let activateAccountType = "net.shadowfacts.Reader.activity.activate-account" static let activateAccountType = "net.shadowfacts.Reader.activity.activate-account"
static let readUnreadType = "net.shadowfacts.Reader.activity.read-unread"
static let readAllType = "net.shadowfacts.Reader.activity.read-all"
static let readFeedType = "net.shadowfacts.Reader.activity.read-feed"
static let readGroupType = "net.shadowfacts.Reader.activity.read-group"
func accountID() -> Data? {
let types = [
NSUserActivity.activateAccountType,
NSUserActivity.readUnreadType,
NSUserActivity.readAllType,
]
if types.contains(self.activityType),
let id = self.userInfo?["accountID"] as? Data {
return id
} else {
return nil
}
}
func feedID() -> String? {
if activityType == NSUserActivity.readFeedType {
return userInfo?["feedID"] as? String
} else {
return nil
}
}
func groupID() -> String? {
if activityType == NSUserActivity.readGroupType {
return userInfo?["groupID"] as? String
} else {
return nil
}
}
static func preferences() -> NSUserActivity { static func preferences() -> NSUserActivity {
return NSUserActivity(activityType: preferencesType) return NSUserActivity(activityType: preferencesType)
@ -58,59 +24,9 @@ extension NSUserActivity {
static func activateAccount(_ account: LocalData.Account) -> NSUserActivity { static func activateAccount(_ account: LocalData.Account) -> NSUserActivity {
let activity = NSUserActivity(activityType: activateAccountType) let activity = NSUserActivity(activityType: activateAccountType)
activity.userInfo = [ activity.userInfo = [
"accountID": account.id, "accountID": account.id.uuidString
] ]
return activity return activity
} }
static func readUnread(account: LocalData.Account) -> NSUserActivity {
let activity = NSUserActivity(activityType: readUnreadType)
activity.isEligibleForHandoff = true
activity.isEligibleForPrediction = true
activity.title = "Show unread articles"
activity.userInfo = [
"accountID": account.id
]
activity.targetContentIdentifier = account.id.base64EncodedString()
return activity
}
static func readAll(account: LocalData.Account) -> NSUserActivity {
let activity = NSUserActivity(activityType: readAllType)
activity.isEligibleForHandoff = true
activity.isEligibleForPrediction = true
activity.title = "Show all articles"
activity.userInfo = [
"accountID": account.id
]
activity.targetContentIdentifier = account.id.base64EncodedString()
return activity
}
static func readFeed(_ feed: Feed, account: LocalData.Account) -> NSUserActivity {
let activity = NSUserActivity(activityType: readFeedType)
activity.isEligibleForHandoff = true
activity.isEligibleForPrediction = true
activity.title = "Show articles from \(feed.title!)"
activity.userInfo = [
"accountID": account.id,
"feedID": feed.id!
]
activity.targetContentIdentifier = account.id.base64EncodedString()
return activity
}
static func readGroup(_ group: Group, account: LocalData.Account) -> NSUserActivity {
let activity = NSUserActivity(activityType: readGroupType)
activity.isEligibleForHandoff = true
activity.isEligibleForPrediction = true
activity.title = "Show articles from \(group.title)"
activity.userInfo = [
"accountID": account.id,
"groupID": group.id!
]
activity.targetContentIdentifier = account.id.base64EncodedString()
return activity
}
} }