Merge pull request #1131 from mastodon/move-credentials-to-keychain

Use Keychain for credentials
This commit is contained in:
Nathan Mattes 2023-10-12 15:09:12 +02:00 committed by GitHub
commit 8381a44b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 564 additions and 492 deletions

View File

@ -331,7 +331,6 @@
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; }; DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; };
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */; }; DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */; };
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */; }; DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */; };
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; };
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; }; DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; };
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; }; DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; };
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; }; DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; };
@ -1037,7 +1036,6 @@
DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+Configuration.swift"; sourceTree = "<group>"; }; DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+Configuration.swift"; sourceTree = "<group>"; };
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Reblog.swift"; sourceTree = "<group>"; }; DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Reblog.swift"; sourceTree = "<group>"; };
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Favorite.swift"; sourceTree = "<group>"; }; DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Favorite.swift"; sourceTree = "<group>"; };
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = "<group>"; };
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = "<group>"; }; DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = "<group>"; };
DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = "<group>"; }; DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = "<group>"; };
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; }; DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; };
@ -2404,7 +2402,6 @@
DB64BA462851F23300ADF1B7 /* Model */ = { DB64BA462851F23300ADF1B7 /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */,
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */, DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */,
); );
path = Model; path = Model;
@ -4057,7 +4054,6 @@
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */, DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */,
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, 2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */, 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */,
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */, D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */,
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */, 2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,

View File

@ -57,12 +57,8 @@ final public class SceneCoordinator {
return return
} else { } else {
// switch to notification's account // switch to notification's account
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
do { do {
guard let authentication = try appContext.managedObjectContext.fetch(request).first else { guard let authentication = AuthenticationServiceProvider.shared.authentications.first(where: { $0.userAccessToken == accessToken }) else {
return return
} }
let domain = authentication.domain let domain = authentication.domain
@ -226,8 +222,7 @@ extension SceneCoordinator {
let rootViewController: UIViewController let rootViewController: UIViewController
do { do {
let request = MastodonAuthentication.activeSortedFetchRequest // use active order let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
let _authentication = try appContext.managedObjectContext.fetch(request).first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
self.authContext = _authContext self.authContext = _authContext
@ -538,7 +533,7 @@ private extension SceneCoordinator {
viewController = activityViewController viewController = activityViewController
case .settings(let setting): case .settings(let setting):
guard let presentedOn = sender, guard let presentedOn = sender,
let accountName = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: appContext.managedObjectContext)?.username, let accountName = authContext?.mastodonAuthenticationBox.authentication.username,
let authContext let authContext
else { return nil } else { return nil }

View File

@ -74,7 +74,7 @@ extension DiscoverySection {
cell.profileCardView.viewModel.familiarFollowers = nil cell.profileCardView.viewModel.familiarFollowers = nil
} }
// bind me // bind me
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
} }
return cell return cell
case .bottomLoader: case .bottomLoader:

View File

@ -80,7 +80,7 @@ extension UserSection {
configuration: Configuration configuration: Configuration
) { ) {
cell.configure( cell.configure(
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate delegate: configuration.userTableViewCellDelegate

View File

@ -12,17 +12,13 @@ import MastodonSDK
extension AppContext { extension AppContext {
func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? { func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? {
let request = MastodonAuthentication.sortedFetchRequest let accounts = AuthenticationServiceProvider.shared.authentications
guard guard accounts.count > 1 else { return nil }
let accounts = try? managedObjectContext.fetch(request),
accounts.count > 1
else { return nil }
let nextSelectedAccountIndex: Int? = { let nextSelectedAccountIndex: Int? = {
for (index, account) in accounts.enumerated() { for (index, account) in accounts.enumerated() {
guard account == authContext.mastodonAuthenticationBox guard account == authContext.mastodonAuthenticationBox
.authenticationRecord .authentication
.object(in: managedObjectContext)
else { continue } else { continue }
let nextAccountIndex = index + 1 let nextAccountIndex = index + 1

View File

@ -24,7 +24,7 @@ extension DataSourceFacade {
let managedObjectContext = provider.context.backgroundManagedObjectContext let managedObjectContext = provider.context.backgroundManagedObjectContext
try? await managedObjectContext.performChanges { try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let user = record.object(in: managedObjectContext) else { return } guard let user = record.object(in: managedObjectContext) else { return }
_ = Persistence.SearchHistory.createOrMerge( _ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
@ -42,7 +42,7 @@ extension DataSourceFacade {
switch tag { switch tag {
case .entity(let entity): case .entity(let entity):
try? await managedObjectContext.performChanges { try? await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let now = Date() let now = Date()
@ -68,7 +68,7 @@ extension DataSourceFacade {
case .record(let record): case .record(let record):
try? await managedObjectContext.performChanges { try? await managedObjectContext.performChanges {
let authenticationBox = provider.authContext.mastodonAuthenticationBox let authenticationBox = provider.authContext.mastodonAuthenticationBox
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
guard let tag = record.object(in: managedObjectContext) else { return } guard let tag = record.object(in: managedObjectContext) else { return }
let now = Date() let now = Date()
@ -99,7 +99,7 @@ extension DataSourceFacade {
let managedObjectContext = provider.context.backgroundManagedObjectContext let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let request = SearchHistory.sortedFetchRequest let request = SearchHistory.sortedFetchRequest
request.predicate = SearchHistory.predicate( request.predicate = SearchHistory.predicate(
domain: authenticationBox.domain, domain: authenticationBox.domain,

View File

@ -21,10 +21,8 @@ final class AccountListViewModel: NSObject {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
// output // output
@Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
@Published var items: [Item] = [] @Published var items: [Item] = []
let dataSourceDidUpdate = PassthroughSubject<Void, Never>() let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
@ -33,30 +31,11 @@ final class AccountListViewModel: NSObject {
init(context: AppContext, authContext: AuthContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.mastodonAuthenticationFetchedResultsController = {
let fetchRequest = MastodonAuthentication.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init() super.init()
// end init // end init
mastodonAuthenticationFetchedResultsController.delegate = self
do {
try mastodonAuthenticationFetchedResultsController.performFetch()
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? []
} catch {
assertionFailure(error.localizedDescription)
}
$authentications AuthenticationServiceProvider.shared.$authentications
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] authentications in .sink { [weak self] authentications in
guard let self = self else { return } guard let self = self else { return }
@ -85,7 +64,7 @@ extension AccountListViewModel {
} }
enum Item: Hashable { enum Item: Hashable {
case authentication(record: ManagedObjectRecord<MastodonAuthentication>) case authentication(record: MastodonAuthentication)
case addAccount case addAccount
} }
@ -97,12 +76,12 @@ extension AccountListViewModel {
switch item { switch item {
case .authentication(let record): case .authentication(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
if let authentication = record.object(in: managedObjectContext), if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
{ {
AccountListViewModel.configure( AccountListViewModel.configure(
in: managedObjectContext,
cell: cell, cell: cell,
authentication: authentication, authentication: record,
activeAuthentication: activeAuthentication activeAuthentication: activeAuthentication
) )
} }
@ -119,11 +98,12 @@ extension AccountListViewModel {
} }
static func configure( static func configure(
in context: NSManagedObjectContext,
cell: AccountListTableViewCell, cell: AccountListTableViewCell,
authentication: MastodonAuthentication, authentication: MastodonAuthentication,
activeAuthentication: MastodonAuthentication activeAuthentication: MastodonAuthentication
) { ) {
let user = authentication.user guard let user = authentication.user(in: context) else { return }
// avatar // avatar
cell.avatarButton.avatarImageView.configure( cell.avatarButton.avatarImageView.configure(
@ -168,16 +148,3 @@ extension AccountListViewModel {
.joined(separator: ", ") .joined(separator: ", ")
} }
} }
// MARK: - NSFetchedResultsControllerDelegate
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? []
}
}

View File

@ -66,8 +66,7 @@ extension AccountListViewController: PanModalPresentable {
return .contentHeight(CGFloat(height)) return .contentHeight(CGFloat(height))
} }
let request = MastodonAuthentication.sortedFetchRequest let authenticationCount = AuthenticationServiceProvider.shared.authentications.count
let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0
let count = authenticationCount + 1 let count = authenticationCount + 1
let height = calculateHeight(of: count) let height = calculateHeight(of: count)
@ -165,9 +164,8 @@ extension AccountListViewController: UITableViewDelegate {
switch item { switch item {
case .authentication(let record): case .authentication(let record):
assert(Thread.isMainThread) assert(Thread.isMainThread)
guard let authentication = record.object(in: context.managedObjectContext) else { return }
Task { @MainActor in Task { @MainActor in
let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) let isActive = try await context.authenticationService.activeMastodonUser(domain: record.domain, userID: record.userID)
guard isActive else { return } guard isActive else { return }
self.coordinator.setup() self.coordinator.setup()
} // end Task } // end Task

View File

@ -211,7 +211,8 @@ extension HomeTimelineViewController {
let userDoesntFollowPeople: Bool let userDoesntFollowPeople: Bool
if let managedObjectContext = self?.context.managedObjectContext, if let managedObjectContext = self?.context.managedObjectContext,
let me = self?.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user { let authContext = self?.authContext,
let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){
userDoesntFollowPeople = me.followersCount == 0 userDoesntFollowPeople = me.followersCount == 0
} else { } else {
userDoesntFollowPeople = true userDoesntFollowPeople = true

View File

@ -292,10 +292,9 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
snapshot.appendSections([MastodonLoginViewSection.servers]) snapshot.appendSections([MastodonLoginViewSection.servers])
snapshot.appendItems(viewModel.filteredServers) snapshot.appendItems(viewModel.filteredServers)
dataSource?.apply(snapshot, animatingDifferences: false)
DispatchQueue.main.async { DispatchQueue.main.async {
self.dataSource?.apply(snapshot, animatingDifferences: false)
let numberOfResults = viewModel.filteredServers.count let numberOfResults = viewModel.filteredServers.count
self.contentView.updateCorners(numberOfResults: numberOfResults) self.contentView.updateCorners(numberOfResults: numberOfResults)
} }

View File

@ -193,41 +193,26 @@ extension AuthenticationViewModel {
domain: info.domain, domain: info.domain,
authorization: authorization authorization: authorization
) )
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in .tryMap { response -> Mastodon.Response.Content<Mastodon.Entity.Account> in
let account = response.value let account = response.value
let mastodonUserRequest = MastodonUser.sortedFetchRequest let mastodonUserRequest = MastodonUser.sortedFetchRequest
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
mastodonUserRequest.fetchLimit = 1 mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher() throw AuthenticationError.badCredentials
} }
AuthenticationServiceProvider.shared
.authentications
.insert(MastodonAuthentication.createFrom(domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret), at: 0)
let property = MastodonAuthentication.Property( return response
domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret
)
return managedObjectContext.performChanges {
_ = APIService.CoreData.createOrMergeMastodonAuthentication(
into: managedObjectContext,
for: mastodonUser,
in: info.domain,
property: property,
networkDate: response.networkDate
)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .failure(let error): throw error
case .success: return response
}
}
.eraseToAnyPublisher()
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View File

@ -33,7 +33,7 @@ final class FollowedTagsViewModel: NSObject {
self.fetchedResultsController = FollowedTagsFetchedResultController( self.fetchedResultsController = FollowedTagsFetchedResultController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain, domain: authContext.mastodonAuthenticationBox.domain,
user: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)!.user user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme:
) )
super.init() super.init()

View File

@ -98,6 +98,7 @@ extension ProfileHeaderView.ViewModel {
// follows you // follows you
$relationshipActionOptionSet $relationshipActionOptionSet
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) } .map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
.receive(on: DispatchQueue.main)
.sink { isFollowingBy in .sink { isFollowingBy in
view.followsYouBlurEffectView.isHidden = !isFollowingBy view.followsYouBlurEffectView.isHidden = !isFollowingBy
} }
@ -182,16 +183,19 @@ extension ProfileHeaderView.ViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
$relationshipActionOptionSet $relationshipActionOptionSet
.receive(on: DispatchQueue.main)
.sink { optionSet in .sink { optionSet in
let isBlocking = optionSet.contains(.blocking) let isBlocking = optionSet.contains(.blocking)
let isBlockedBy = optionSet.contains(.blockingBy) let isBlockedBy = optionSet.contains(.blockingBy)
let isSuspended = optionSet.contains(.suspended) let isSuspended = optionSet.contains(.suspended)
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
view.bioMetaText.textView.isHidden = isNeedsHidden view.bioMetaText.textView.isHidden = isNeedsHidden
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// dashboard // dashboard
$isMyself $isMyself
.receive(on: DispatchQueue.main)
.sink { isMyself in .sink { isMyself in
if isMyself { if isMyself {
view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts
@ -246,6 +250,7 @@ extension ProfileHeaderView.ViewModel {
$isEditing, $isEditing,
$isUpdating $isUpdating
) )
.receive(on: DispatchQueue.main)
.sink { relationshipActionOptionSet, isEditing, isUpdating in .sink { relationshipActionOptionSet, isEditing, isUpdating in
if relationshipActionOptionSet.contains(.edit) { if relationshipActionOptionSet.contains(.edit) {
// check .edit state and set .editing when isEditing // check .edit state and set .editing when isEditing

View File

@ -15,7 +15,7 @@ import MastodonSDK
final class MeProfileViewModel: ProfileViewModel { final class MeProfileViewModel: ProfileViewModel {
init(context: AppContext, authContext: AuthContext) { init(context: AppContext, authContext: AuthContext) {
let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
super.init( super.init(
context: context, context: context,
authContext: authContext, authContext: authContext,
@ -29,5 +29,27 @@ final class MeProfileViewModel: ProfileViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
_ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
try await context.managedObjectContext.performChanges {
guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else {
assertionFailure()
return
}
self.me = me
}
} catch {
// do nothing?
}
}
}
} }

View File

@ -273,6 +273,8 @@ extension ProfileViewController {
bindTitleView() bindTitleView()
bindMoreBarButtonItem() bindMoreBarButtonItem()
bindPager() bindPager()
viewModel.viewDidLoad()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -935,11 +937,6 @@ extension ProfileViewController: PagerTabStripNavigateable {
private extension ProfileViewController { private extension ProfileViewController {
var currentInstance: Instance? { var currentInstance: Instance? {
guard let authenticationRecord = authContext.mastodonAuthenticationBox authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext)
.authenticationRecord
.object(in: context.managedObjectContext)
else { return nil }
return authenticationRecord.instance
} }
} }

View File

@ -82,7 +82,7 @@ class ProfileViewModel: NSObject {
super.init() super.init()
// bind me // bind me
self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
$me $me
.assign(to: \.me, on: relationshipViewModel) .assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
@ -171,9 +171,10 @@ class ProfileViewModel: NSObject {
.assign(to: &$isPagingEnabled) .assign(to: &$isPagingEnabled)
} }
}
extension ProfileViewModel { func viewDidLoad() {
}
// fetch profile info before edit // fetch profile info before edit
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> { func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {

View File

@ -59,7 +59,7 @@ class ReportResultViewModel: ObservableObject {
Task { @MainActor in Task { @MainActor in
guard let user = user.object(in: context.managedObjectContext) else { return } guard let user = user.object(in: context.managedObjectContext) else { return }
guard let me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { return } guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return }
self.relationshipViewModel.user = user self.relationshipViewModel.user = user
self.relationshipViewModel.me = me self.relationshipViewModel.me = me

View File

@ -258,28 +258,34 @@ extension MainTabBarController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { NotificationCenter.default.publisher(for: .userFetched)
self.avatarURLObserver = user.publisher(for: \.avatar) .receive(on: DispatchQueue.main)
.sink { [weak self, weak user] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
guard let user = user else { return } if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
guard user.managedObjectContext != nil else { return } self.avatarURLObserver = user.publisher(for: \.avatar)
self.avatarURL = user.avatarImageURL() .sink { [weak self, weak user] _ in
} guard let self = self else { return }
guard let user = user else { return }
guard user.managedObjectContext != nil else { return }
self.avatarURL = user.avatarImageURL()
}
// a11y // a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return } guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback) profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
context.authenticationService.updateActiveUserAccountPublisher self.context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in .sink { [weak self] in
self?.updateUserAccount() self?.updateUserAccount()
}
.store(in: &self.disposeBag)
} else {
self.avatarURLObserver = nil
} }
.store(in: &disposeBag) }
} else { .store(in: &disposeBag)
self.avatarURLObserver = nil
}
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
@ -451,9 +457,9 @@ extension MainTabBarController {
authenticationBox: authContext.mastodonAuthenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object( if let user = authContext.mastodonAuthenticationBox.authentication.user(
in: context.managedObjectContext in: context.managedObjectContext
)?.user { ) {
user.update( user.update(
property: .init( property: .init(
entity: profileResponse.value, entity: profileResponse.value,

View File

@ -75,7 +75,7 @@ extension SidebarViewModel {
let imageURL: URL? = { let imageURL: URL? = {
switch item { switch item {
case .me: case .me:
let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext)
return user?.avatarImageURL() return user?.avatarImageURL()
default: default:
return nil return nil
@ -132,7 +132,7 @@ extension SidebarViewModel {
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
case .me: case .me:
guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
let currentUserDisplayName = user.displayNameWithFallback let currentUserDisplayName = user.displayNameWithFallback
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
default: default:

View File

@ -79,7 +79,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext(
domain: authContext.mastodonAuthenticationBox.domain, domain: authContext.mastodonAuthenticationBox.domain,
entity: status, entity: status,
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext),
statusCache: nil, statusCache: nil,
userCache: nil, userCache: nil,
networkDate: Date())) networkDate: Date()))

View File

@ -126,7 +126,7 @@ extension SearchResultSection {
configuration: Configuration configuration: Configuration
) { ) {
cell.configure( cell.configure(
me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate delegate: configuration.userTableViewCellDelegate

View File

@ -17,6 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let appContext = AppContext() let appContext = AppContext()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AuthenticationServiceProvider.shared.restore()
AppSecret.default.register() AppSecret.default.register()

View File

@ -185,11 +185,8 @@ extension SceneDelegate {
assertionFailure() assertionFailure()
return false return false
} }
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) guard let authentication = AuthenticationServiceProvider.shared.getAuthentication(matching: accessToken) else {
request.fetchLimit = 1
guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else {
assertionFailure() assertionFailure()
return false return false
} }

View File

@ -50,9 +50,8 @@ extension SendPostIntentHandler: SendPostIntentHandling {
let mastodonAuthentications: [MastodonAuthentication] let mastodonAuthentications: [MastodonAuthentication]
let accounts = intent.accounts ?? [] let accounts = intent.accounts ?? []
if accounts.isEmpty { if accounts.isEmpty {
let request = MastodonAuthentication.sortedFetchRequest // fixme: refactor this and implemented method on AuthenticationServiceProvider
let authentications = try managedObjectContext.fetch(request) let _authentication = AuthenticationServiceProvider.shared.authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
let _authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
guard let authentication = _authentication else { guard let authentication = _authentication else {
let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post" let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post"
@ -65,12 +64,12 @@ extension SendPostIntentHandler: SendPostIntentHandling {
let authenticationBoxes = mastodonAuthentications.map { authentication in let authenticationBoxes = mastodonAuthentications.map { authentication in
MastodonAuthenticationBox( MastodonAuthenticationBox(
authenticationRecord: .init(objectID: authentication.objectID), authentication: authentication,
domain: authentication.domain, domain: authentication.domain,
userID: authentication.userID, userID: authentication.userID,
appAuthorization: .init(accessToken: authentication.appAccessToken), appAuthorization: .init(accessToken: authentication.appAccessToken),
userAuthorization: .init(accessToken: authentication.userAccessToken), userAuthorization: .init(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description) inMemoryCache: .sharedCache(for: authentication.userAccessToken)
) )
} }

View File

@ -17,9 +17,11 @@ extension Account {
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] { static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
// get accounts // get accounts
let accounts: [Account] = try await managedObjectContext.perform { let accounts: [Account] = try await managedObjectContext.perform {
let results = try MastodonAuthentication.fetch(in: managedObjectContext) let results = AuthenticationServiceProvider.shared.authentications
let accounts = results.compactMap { mastodonAuthentication -> Account? in let accounts = results.compactMap { mastodonAuthentication -> Account? in
let user = mastodonAuthentication.user guard let user = mastodonAuthentication.user(in: managedObjectContext) else {
return nil
}
let account = Account( let account = Account(
identifier: mastodonAuthentication.identifier.uuidString, identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback, display: user.displayNameWithFallback,
@ -43,9 +45,7 @@ extension Array where Element == Account {
let identifiers = self let identifiers = self
.compactMap { $0.identifier } .compactMap { $0.identifier }
.compactMap { UUID(uuidString: $0) } .compactMap { UUID(uuidString: $0) }
let request = MastodonAuthentication.sortedFetchRequest let results = AuthenticationServiceProvider.shared.authentications.filter({ identifiers.contains($0.identifier) })
request.predicate = MastodonAuthentication.predicate(identifiers: identifiers)
let results = try managedObjectContext.fetch(request)
return results return results
} }

View File

@ -1,20 +0,0 @@
//
// MastodonAuthentication.swift
// MastodonIntent
//
// Created by MainasuK on 2022-6-9.
//
import Foundation
import CoreData
import CoreDataStack
extension MastodonAuthentication {
static func fetch(in managedObjectContext: NSManagedObjectContext) throws -> [MastodonAuthentication] {
let request = MastodonAuthentication.sortedFetchRequest
let results = try managedObjectContext.fetch(request)
return results
}
}

View File

@ -65,7 +65,7 @@
<attribute name="version" optional="YES" attributeType="String"/> <attribute name="version" optional="YES" attributeType="String"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/> <relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity> </entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES"> <entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthenticationLegacy" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/> <attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/> <attribute name="clientID" attributeType="String"/>

View File

@ -65,7 +65,7 @@
<attribute name="version" optional="YES" attributeType="String"/> <attribute name="version" optional="YES" attributeType="String"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/> <relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity> </entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES"> <entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthenticationLegacy" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/> <attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/> <attribute name="clientID" attributeType="String"/>

View File

@ -20,9 +20,14 @@ public final class CoreDataStack {
self.storeDescriptions = storeDescriptions self.storeDescriptions = storeDescriptions
} }
public convenience init(databaseName: String = "shared") { public convenience init(databaseName: String = "shared", isInMemory: Bool = false) {
let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName) let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL) let storeDescription: NSPersistentStoreDescription
if isInMemory {
storeDescription = NSPersistentStoreDescription(url: URL(string: "file:///dev/null")!) /// in-memory store with all features in favor of NSInMemoryStoreType
} else {
storeDescription = NSPersistentStoreDescription(url: storeURL)
}
self.init(persistentStoreDescriptions: [storeDescription]) self.init(persistentStoreDescriptions: [storeDescription])
} }
@ -115,16 +120,18 @@ extension CoreDataStack {
} }
} }
extension CoreDataStack { public extension CoreDataStack {
func tearDown() {
public func rebuild() {
let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!) let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!)
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil) try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil)
}
func rebuild() {
tearDown()
CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.didFinishLoad.value = true self.didFinishLoad.value = true
} }
} }
} }

View File

@ -19,7 +19,7 @@ public final class Instance: NSManagedObject {
@NSManaged public private(set) var configurationV2Raw: Data? @NSManaged public private(set) var configurationV2Raw: Data?
// MARK: one-to-many relationships // MARK: one-to-many relationships
@NSManaged public var authentications: Set<MastodonAuthentication> @NSManaged public var authentications: Set<MastodonAuthenticationLegacy>
} }
extension Instance { extension Instance {

View File

@ -8,7 +8,8 @@
import Foundation import Foundation
import CoreData import CoreData
final public class MastodonAuthentication: NSManagedObject { @objc(MastodonAuthentication)
final public class MastodonAuthenticationLegacy: NSManagedObject {
public typealias ID = UUID public typealias ID = UUID
@ -35,16 +36,16 @@ final public class MastodonAuthentication: NSManagedObject {
} }
extension MastodonAuthentication { extension MastodonAuthenticationLegacy {
public override func awakeFromInsert() { public override func awakeFromInsert() {
super.awakeFromInsert() super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthentication.identifier)) setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthenticationLegacy.identifier))
let now = Date() let now = Date()
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.createdAt)) setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.createdAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.updatedAt)) setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.updatedAt))
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.activedAt)) setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.activedAt))
} }
@discardableResult @discardableResult
@ -52,8 +53,8 @@ extension MastodonAuthentication {
into context: NSManagedObjectContext, into context: NSManagedObjectContext,
property: Property, property: Property,
user: MastodonUser user: MastodonUser
) -> MastodonAuthentication { ) -> MastodonAuthenticationLegacy {
let authentication: MastodonAuthentication = context.insertObject() let authentication: MastodonAuthenticationLegacy = context.insertObject()
authentication.domain = property.domain authentication.domain = property.domain
authentication.userID = property.userID authentication.userID = property.userID
@ -112,7 +113,7 @@ extension MastodonAuthentication {
} }
extension MastodonAuthentication { extension MastodonAuthenticationLegacy {
public struct Property { public struct Property {
public let domain: String public let domain: String
@ -144,51 +145,51 @@ extension MastodonAuthentication {
} }
} }
extension MastodonAuthentication: Managed { extension MastodonAuthenticationLegacy: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] { public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)] return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.createdAt, ascending: false)]
} }
public static var activeSortDescriptors: [NSSortDescriptor] { public static var activeSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \MastodonAuthentication.activedAt, ascending: false)] return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.activedAt, ascending: false)]
} }
} }
extension MastodonAuthentication { extension MastodonAuthenticationLegacy {
public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthentication> { public static var activeSortedFetchRequest: NSFetchRequest<MastodonAuthenticationLegacy> {
let request = NSFetchRequest<MastodonAuthentication>(entityName: entityName) let request = NSFetchRequest<MastodonAuthenticationLegacy>(entityName: entityName)
request.sortDescriptors = activeSortDescriptors request.sortDescriptors = activeSortDescriptors
return request return request
} }
} }
extension MastodonAuthentication { extension MastodonAuthenticationLegacy {
public static func predicate(domain: String) -> NSPredicate { public static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain) return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.domain), domain)
} }
static func predicate(userID: String) -> NSPredicate { static func predicate(userID: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userID), userID) return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userID), userID)
} }
public static func predicate(domain: String, userID: String) -> NSPredicate { public static func predicate(domain: String, userID: String) -> NSPredicate {
return NSCompoundPredicate(andPredicateWithSubpredicates: [ return NSCompoundPredicate(andPredicateWithSubpredicates: [
MastodonAuthentication.predicate(domain: domain), MastodonAuthenticationLegacy.predicate(domain: domain),
MastodonAuthentication.predicate(userID: userID) MastodonAuthenticationLegacy.predicate(userID: userID)
]) ])
} }
public static func predicate(userAccessToken: String) -> NSPredicate { public static func predicate(userAccessToken: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userAccessToken), userAccessToken) return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userAccessToken), userAccessToken)
} }
public static func predicate(identifier: UUID) -> NSPredicate { public static func predicate(identifier: UUID) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.identifier), identifier as NSUUID) return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifier as NSUUID)
} }
public static func predicate(identifiers: [UUID]) -> NSPredicate { public static func predicate(identifiers: [UUID]) -> NSPredicate {
return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthentication.identifier), identifiers as [NSUUID]) return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifiers as [NSUUID])
} }
} }

View File

@ -62,7 +62,7 @@ final public class MastodonUser: NSManagedObject {
// one-to-one relationship // one-to-one relationship
@NSManaged public private(set) var pinnedStatus: Status? @NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication? @NSManaged public private(set) var mastodonAuthentication: MastodonAuthenticationLegacy?
// one-to-many relationship // one-to-many relationship
@NSManaged public private(set) var statuses: Set<Status> @NSManaged public private(set) var statuses: Set<Status>

View File

@ -46,9 +46,18 @@ public class AppContext: ObservableObject {
.eraseToAnyPublisher() .eraseToAnyPublisher()
public init() { public init() {
let authProvider = AuthenticationServiceProvider.shared
let _coreDataStack = CoreDataStack() let _coreDataStack = CoreDataStack()
if authProvider.authenticationMigrationRequired {
authProvider.migrateLegacyAuthentications(
in: _coreDataStack.persistentContainer.viewContext
)
}
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
coreDataStack = _coreDataStack coreDataStack = _coreDataStack
managedObjectContext = _managedObjectContext managedObjectContext = _managedObjectContext
backgroundManagedObjectContext = _backgroundManagedObjectContext backgroundManagedObjectContext = _backgroundManagedObjectContext

View File

@ -24,31 +24,12 @@ public class AuthContext {
private init(mastodonAuthenticationBox: MastodonAuthenticationBox) { private init(mastodonAuthenticationBox: MastodonAuthenticationBox) {
self.mastodonAuthenticationBox = mastodonAuthenticationBox self.mastodonAuthenticationBox = mastodonAuthenticationBox
} }
} }
extension AuthContext { extension AuthContext {
public convenience init?(authentication: MastodonAuthentication) { public convenience init?(authentication: MastodonAuthentication) {
self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication)) self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication))
ManagedObjectObserver.observe(object: authentication)
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] change in
guard let self = self else { return }
switch change.changeType {
case .update(let object):
guard let authentication = object as? MastodonAuthentication else {
assertionFailure()
return
}
self.mastodonAuthenticationBox = .init(authentication: authentication)
default:
break
}
}
.store(in: &disposeBag)
} }
} }

View File

@ -10,7 +10,7 @@ import CoreDataStack
import MastodonSDK import MastodonSDK
public struct MastodonAuthenticationBox: UserIdentifier { public struct MastodonAuthenticationBox: UserIdentifier {
public let authenticationRecord: ManagedObjectRecord<MastodonAuthentication> public let authentication: MastodonAuthentication
public let domain: String public let domain: String
public let userID: MastodonUser.ID public let userID: MastodonUser.ID
public let appAuthorization: Mastodon.API.OAuth.Authorization public let appAuthorization: Mastodon.API.OAuth.Authorization
@ -18,14 +18,14 @@ public struct MastodonAuthenticationBox: UserIdentifier {
public let inMemoryCache: MastodonAccountInMemoryCache public let inMemoryCache: MastodonAccountInMemoryCache
public init( public init(
authenticationRecord: ManagedObjectRecord<MastodonAuthentication>, authentication: MastodonAuthentication,
domain: String, domain: String,
userID: MastodonUser.ID, userID: MastodonUser.ID,
appAuthorization: Mastodon.API.OAuth.Authorization, appAuthorization: Mastodon.API.OAuth.Authorization,
userAuthorization: Mastodon.API.OAuth.Authorization, userAuthorization: Mastodon.API.OAuth.Authorization,
inMemoryCache: MastodonAccountInMemoryCache inMemoryCache: MastodonAccountInMemoryCache
) { ) {
self.authenticationRecord = authenticationRecord self.authentication = authentication
self.domain = domain self.domain = domain
self.userID = userID self.userID = userID
self.appAuthorization = appAuthorization self.appAuthorization = appAuthorization
@ -38,12 +38,12 @@ extension MastodonAuthenticationBox {
init(authentication: MastodonAuthentication) { init(authentication: MastodonAuthentication) {
self = MastodonAuthenticationBox( self = MastodonAuthenticationBox(
authenticationRecord: .init(objectID: authentication.objectID), authentication: authentication,
domain: authentication.domain, domain: authentication.domain,
userID: authentication.userID, userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken), userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description) inMemoryCache: .sharedCache(for: authentication.userID) // todo: make sure this is really unique
) )
} }

View File

@ -0,0 +1,116 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import Combine
import CoreDataStack
import MastodonSDK
import KeychainAccess
import MastodonCommon
import os.log
public class AuthenticationServiceProvider: ObservableObject {
private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication")
public static let shared = AuthenticationServiceProvider()
private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID)
private let userDefaults: UserDefaults = .shared
private init() {}
@Published public var authentications: [MastodonAuthentication] = [] {
didSet {
persist() // todo: Is this too heavy and too often here???
}
}
func update(instance: Instance, where domain: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication }
return authentication.updating(instance: instance)
}
}
func delete(authentication: MastodonAuthentication) {
authentications.removeAll(where: { $0 == authentication })
}
func activateAuthentication(in domain: String, for userID: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain, authentication.userID == userID else {
return authentication
}
return authentication.updating(activatedAt: Date())
}
}
func getAuthentication(in domain: String, for userID: String) -> MastodonAuthentication? {
authentications.first(where: { $0.domain == domain && $0.userID == userID })
}
}
// MARK: - Public
public extension AuthenticationServiceProvider {
func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? {
authentications.first(where: { $0.userAccessToken == userAccessToken })
}
func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
}
func restore() {
authentications = Self.keychain.allKeys().compactMap {
guard
let encoded = Self.keychain[$0],
let data = Data(base64Encoded: encoded)
else { return nil }
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
}
}
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
do {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in
return MastodonAuthentication(
identifier: auth.identifier,
domain: auth.domain,
username: auth.username,
appAccessToken: auth.appAccessToken,
userAccessToken: auth.userAccessToken,
clientID: auth.clientID,
clientSecret: auth.clientSecret,
createdAt: auth.createdAt,
updatedAt: auth.updatedAt,
activedAt: auth.activedAt,
userID: auth.userID
)
}
if migratedAuthentications.count != legacyAuthentications.count {
logger.log(level: .default, "Not all account authentications could be migrated.")
} else {
logger.log(level: .default, "All account authentications were successful.")
}
self.authentications = migratedAuthentications
userDefaults.didMigrateAuthentications = true
} catch {
userDefaults.didMigrateAuthentications = false
logger.log(level: .error, "Could not migrate legacy authentications")
}
}
var authenticationMigrationRequired: Bool {
userDefaults.didMigrateAuthentications == false
}
}
// MARK: - Private
private extension AuthenticationServiceProvider {
func persist() {
for authentication in authentications {
Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString()
}
}
}

View File

@ -0,0 +1,107 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreDataStack
import MastodonSDK
public struct MastodonAuthentication: Codable, Hashable {
public typealias ID = UUID
public private(set) var identifier: ID
public private(set) var domain: String
public private(set) var username: String
public private(set) var appAccessToken: String
public private(set) var userAccessToken: String
public private(set) var clientID: String
public private(set) var clientSecret: String
public private(set) var createdAt: Date
public private(set) var updatedAt: Date
public private(set) var activedAt: Date
public private(set) var userID: String
public private(set) var instanceObjectIdURI: URL?
internal var persistenceIdentifier: String {
"\(username)@\(domain)"
}
public static func createFrom(
domain: String,
userID: String,
username: String,
appAccessToken: String,
userAccessToken: String,
clientID: String,
clientSecret: String
) -> Self {
let now = Date()
return MastodonAuthentication(
identifier: .init(),
domain: domain,
username: username,
appAccessToken: appAccessToken,
userAccessToken: userAccessToken,
clientID: clientID,
clientSecret: clientSecret,
createdAt: now,
updatedAt: now,
activedAt: now,
userID: userID,
instanceObjectIdURI: nil
)
}
func copy(
identifier: ID? = nil,
domain: String? = nil,
username: String? = nil,
appAccessToken: String? = nil,
userAccessToken: String? = nil,
clientID: String? = nil,
clientSecret: String? = nil,
createdAt: Date? = nil,
updatedAt: Date? = nil,
activedAt: Date? = nil,
userID: String? = nil,
instanceObjectIdURI: URL? = nil
) -> Self {
MastodonAuthentication(
identifier: identifier ?? self.identifier,
domain: domain ?? self.domain,
username: username ?? self.username,
appAccessToken: appAccessToken ?? self.appAccessToken,
userAccessToken: userAccessToken ?? self.userAccessToken,
clientID: clientID ?? self.clientID,
clientSecret: clientSecret ?? self.clientSecret,
createdAt: createdAt ?? self.createdAt,
updatedAt: updatedAt ?? self.updatedAt,
activedAt: activedAt ?? self.activedAt,
userID: userID ?? self.userID,
instanceObjectIdURI: instanceObjectIdURI ?? self.instanceObjectIdURI
)
}
public func instance(in context: NSManagedObjectContext) -> Instance? {
guard
let instanceObjectIdURI = instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else { return nil }
return try? context.existingObject(with: objectID) as? Instance
}
public func user(in context: NSManagedObjectContext) -> MastodonUser? {
let userPredicate = MastodonUser.predicate(domain: domain, id: userID)
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
func updating(instance: Instance) -> Self {
copy(instanceObjectIdURI: instance.objectID.uriRepresentation())
}
func updating(activatedAt: Date) -> Self {
copy(activedAt: activatedAt)
}
}

View File

@ -167,7 +167,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
_ = Persistence.Tag.createOrMerge( _ = Persistence.Tag.createOrMerge(

View File

@ -67,12 +67,15 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { else {
throw APIError.implicit(.badRequest) throw APIError.implicit(.badRequest)
} }
let me = authentication.user
let isBlocking = user.blockingBy.contains(me) let isBlocking = user.blockingBy.contains(me)
let isFollowing = user.followingBy.contains(me) let isFollowing = user.followingBy.contains(me)
// toggle block state // toggle block state
@ -116,10 +119,13 @@ extension APIService {
} }
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return } else { return }
let me = authentication.user
switch result { switch result {
case .success(let response): case .success(let response):

View File

@ -27,12 +27,15 @@ extension APIService {
// update bookmark state and retrieve bookmark context // update bookmark state and retrieve bookmark context
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { else {
throw APIError.implicit(.badRequest) throw APIError.implicit(.badRequest)
} }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
let isBookmarked = status.bookmarkedBy.contains(me) let isBookmarked = status.bookmarkedBy.contains(me)
status.update(bookmarked: !isBookmarked, by: me) status.update(bookmarked: !isBookmarked, by: me)
@ -60,10 +63,13 @@ extension APIService {
// update bookmark state // update bookmark state
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return } else { return }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
switch result { switch result {
@ -108,7 +114,10 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else {
guard
let me = authenticationBox.authentication.user(in: managedObjectContext)
else {
assertionFailure() assertionFailure()
return return
} }

View File

@ -28,12 +28,15 @@ extension APIService {
// update like state and retrieve like context // update like state and retrieve like context
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges { let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { else {
throw APIError.implicit(.badRequest) throw APIError.implicit(.badRequest)
} }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
let isFavorited = status.favouritedBy.contains(me) let isFavorited = status.favouritedBy.contains(me)
let favoritedCount = status.favouritesCount let favoritedCount = status.favouritesCount
@ -65,10 +68,13 @@ extension APIService {
// update like state // update like state
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return } else { return }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
switch result { switch result {
@ -117,7 +123,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure() assertionFailure()
return return
} }

View File

@ -36,7 +36,7 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return nil } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil }
guard let user = user.object(in: managedObjectContext) else { return nil } guard let user = user.object(in: managedObjectContext) else { return nil }
let isFollowing = user.followingBy.contains(me) let isFollowing = user.followingBy.contains(me)
@ -88,7 +88,7 @@ extension APIService {
// update friendship state // update friendship state
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, guard let me = authenticationBox.authentication.user(in: managedObjectContext),
let user = user.object(in: managedObjectContext) let user = user.object(in: managedObjectContext)
else { return } else { return }
@ -120,10 +120,9 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext), guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) let me = authenticationBox.authentication.user(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) } else { throw APIError.implicit(.badRequest) }
let me = authentication.user
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let oldShowReblogs = me.showingReblogsBy.contains(user) let oldShowReblogs = me.showingReblogsBy.contains(user)
@ -144,7 +143,7 @@ extension APIService {
} }
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
switch result { switch result {
case .success(let response): case .success(let response):

View File

@ -35,7 +35,7 @@ extension APIService {
) )
request.fetchLimit = 1 request.fetchLimit = 1
guard let user = managedObjectContext.safeFetch(request).first else { return } guard let user = managedObjectContext.safeFetch(request).first else { return }
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
Persistence.MastodonUser.update( Persistence.MastodonUser.update(
mastodonUser: user, mastodonUser: user,

View File

@ -35,7 +35,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge( let result = Persistence.MastodonUser.createOrMerge(

View File

@ -36,8 +36,8 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge( let result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext, in: managedObjectContext,

View File

@ -44,8 +44,8 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
_ = Persistence.Status.createOrMerge( _ = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,

View File

@ -11,6 +11,10 @@ import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK import MastodonSDK
public extension Foundation.Notification.Name {
static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched")
}
extension APIService { extension APIService {
public func homeTimeline( public func homeTimeline(
@ -38,9 +42,21 @@ extension APIService {
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
// FIXME: This is a dirty hack to make the performance-stuff work.
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
// it when we need it to display on the home timeline.
for authentication in AuthenticationServiceProvider.shared.authentications {
_ = try await accountInfo(domain: authentication.domain,
userID: authentication.userID,
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value
}
NotificationCenter.default.post(name: .userFetched, object: nil)
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure() assertionFailure()
return return
} }

View File

@ -66,13 +66,15 @@ extension APIService {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges { let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext)
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { else {
throw APIError.implicit(.badRequest) throw APIError.implicit(.badRequest)
} }
let me = authentication.user
let isMuting = user.mutingBy.contains(me) let isMuting = user.mutingBy.contains(me)
// toggle mute state // toggle mute state
@ -112,9 +114,8 @@ extension APIService {
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext), guard let user = user.object(in: managedObjectContext),
let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) let me = authenticationBox.authentication.user(in: managedObjectContext)
else { return } else { return }
let me = authentication.user
switch result { switch result {
case .success(let response): case .success(let response):

View File

@ -88,7 +88,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure() assertionFailure()
return return
} }
@ -176,7 +176,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
_ = Persistence.Notification.createOrMerge( _ = Persistence.Notification.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Notification.PersistContext( context: Persistence.Notification.PersistContext(

View File

@ -35,7 +35,7 @@ extension APIService {
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge( _ = Persistence.Poll.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Poll.PersistContext( context: Persistence.Poll.PersistContext(
@ -78,7 +78,7 @@ extension APIService {
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge( _ = Persistence.Poll.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Poll.PersistContext( context: Persistence.Poll.PersistContext(

View File

@ -29,7 +29,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
_ = Persistence.Status.createOrMerge( _ = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,

View File

@ -27,11 +27,13 @@ extension APIService {
// update repost state and retrieve repost context // update repost state and retrieve repost context
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return nil } else { return nil }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
let isReblogged = status.rebloggedBy.contains(me) let isReblogged = status.rebloggedBy.contains(me)
let rebloggedCount = status.reblogsCount let rebloggedCount = status.reblogsCount
@ -66,10 +68,13 @@ extension APIService {
// update repost state // update repost state
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), let authentication = authenticationBox.authentication
let _status = record.object(in: managedObjectContext)
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return } else { return }
let me = authentication.user
let status = _status.reblog ?? _status let status = _status.reblog ?? _status
switch result { switch result {

View File

@ -41,7 +41,7 @@ extension APIService {
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
// assertionFailure() // assertionFailure()
return return
} }

View File

@ -27,7 +27,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
// user // user
for entity in response.value.accounts { for entity in response.value.accounts {

View File

@ -76,7 +76,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
let status = Persistence.Status.createOrMerge( let status = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Status.PersistContext( context: Persistence.Status.PersistContext(

View File

@ -33,7 +33,7 @@ extension APIService {
#if !APP_EXTENSION #if !APP_EXTENSION
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge( _ = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Status.PersistContext( context: Persistence.Status.PersistContext(

View File

@ -29,7 +29,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge( _ = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,
context: Persistence.Status.PersistContext( context: Persistence.Status.PersistContext(

View File

@ -73,7 +73,7 @@ fileprivate extension APIService {
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Tag.createOrMerge( _ = Persistence.Tag.createOrMerge(
in: managedObjectContext, in: managedObjectContext,

View File

@ -29,7 +29,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
let value = response.value.ancestors + response.value.descendants let value = response.value.ancestors + response.value.descendants
for entity in value { for entity in value {

View File

@ -45,7 +45,7 @@ extension APIService {
let managedObjectContext = self.backgroundManagedObjectContext let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges { try await managedObjectContext.performChanges {
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value { for entity in response.value {
_ = Persistence.Status.createOrMerge( _ = Persistence.Status.createOrMerge(
in: managedObjectContext, in: managedObjectContext,

View File

@ -1,75 +0,0 @@
//
// APIService+CoreData+MastodonAuthentication.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.
//
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService.CoreData {
public static func createOrMergeMastodonAuthentication(
into managedObjectContext: NSManagedObjectContext,
for authenticateMastodonUser: MastodonUser,
in domain: String,
property: MastodonAuthentication.Property,
networkDate: Date
) -> (mastodonAuthentication: MastodonAuthentication, isCreated: Bool) {
// fetch old mastodon authentication
let oldMastodonAuthentication: MastodonAuthentication? = {
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: property.userID)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()
if let oldMastodonAuthentication = oldMastodonAuthentication {
// merge old mastodon authentication
APIService.CoreData.mergeMastodonAuthentication(
for: authenticateMastodonUser,
old: oldMastodonAuthentication,
in: domain,
property: property,
networkDate: networkDate
)
return (oldMastodonAuthentication, false)
} else {
let mastodonAuthentication = MastodonAuthentication.insert(
into: managedObjectContext,
property: property,
user: authenticateMastodonUser
)
return (mastodonAuthentication, true)
}
}
static func mergeMastodonAuthentication(
for authenticateMastodonUser: MastodonUser,
old authentication: MastodonAuthentication,
in domain: String,
property: MastodonAuthentication.Property,
networkDate: Date
) {
guard networkDate > authentication.updatedAt else { return }
authentication.update(username: property.username)
authentication.update(appAccessToken: property.appAccessToken)
authentication.update(userAccessToken: property.userAccessToken)
authentication.update(clientID: property.clientID)
authentication.update(clientSecret: property.clientSecret)
authentication.didUpdate(at: networkDate)
}
}

View File

@ -21,10 +21,9 @@ public final class AuthenticationService: NSObject {
weak var apiService: APIService? weak var apiService: APIService?
let managedObjectContext: NSManagedObjectContext // read-only let managedObjectContext: NSManagedObjectContext // read-only
let backgroundManagedObjectContext: NSManagedObjectContext let backgroundManagedObjectContext: NSManagedObjectContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication> let authenticationServiceProvider = AuthenticationServiceProvider.shared
// output // output
@Published public var mastodonAuthentications: [ManagedObjectRecord<MastodonAuthentication>] = []
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
private func fetchFollowedBlockedUserIds( private func fetchFollowedBlockedUserIds(
@ -92,21 +91,8 @@ public final class AuthenticationService: NSObject {
self.managedObjectContext = managedObjectContext self.managedObjectContext = managedObjectContext
self.backgroundManagedObjectContext = backgroundManagedObjectContext self.backgroundManagedObjectContext = backgroundManagedObjectContext
self.apiService = apiService self.apiService = apiService
self.mastodonAuthenticationFetchedResultsController = {
let fetchRequest = MastodonAuthentication.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init()
mastodonAuthenticationFetchedResultsController.delegate = self super.init()
$mastodonAuthenticationBoxes $mastodonAuthenticationBoxes
.sink { [weak self] boxes in .sink { [weak self] boxes in
@ -122,10 +108,9 @@ public final class AuthenticationService: NSObject {
// TODO: verify credentials for active authentication // TODO: verify credentials for active authentication
$mastodonAuthentications authenticationServiceProvider.$authentications
.map { authentications -> [MastodonAuthenticationBox] in .map { authentications -> [MastodonAuthenticationBox] in
return authentications return authentications
.compactMap { $0.object(in: managedObjectContext) }
.sorted(by: { $0.activedAt > $1.activedAt }) .sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { authentication -> MastodonAuthenticationBox? in .compactMap { authentication -> MastodonAuthenticationBox? in
return MastodonAuthenticationBox(authentication: authentication) return MastodonAuthenticationBox(authentication: authentication)
@ -133,14 +118,7 @@ public final class AuthenticationService: NSObject {
} }
.assign(to: &$mastodonAuthenticationBoxes) .assign(to: &$mastodonAuthenticationBoxes)
do { AuthenticationServiceProvider.shared.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation()
try mastodonAuthenticationFetchedResultsController.performFetch()
mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?
.sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { $0.asRecord } ?? []
} catch {
assertionFailure(error.localizedDescription)
}
} }
} }
@ -150,18 +128,9 @@ extension AuthenticationService {
public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool {
var isActive = false var isActive = false
let managedObjectContext = backgroundManagedObjectContext AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID)
try await managedObjectContext.performChanges { isActive = true
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
request.fetchLimit = 1
guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else {
return
}
mastodonAuthentication.update(activedAt: Date())
isActive = true
}
return isActive return isActive
} }
@ -182,12 +151,7 @@ extension AuthenticationService {
managedObjectContext.delete(feed) managedObjectContext.delete(feed)
} }
guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) else { AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication)
assertionFailure()
throw APIService.APIError.implicit(.authenticationMissing)
}
managedObjectContext.delete(authentication)
} }
// cancel push notification subscription // cancel push notification subscription
@ -202,19 +166,3 @@ extension AuthenticationService {
} }
} }
// MARK: - NSFetchedResultsControllerDelegate
extension AuthenticationService: NSFetchedResultsControllerDelegate {
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?
.sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { $0.asRecord } ?? []
}
}

View File

@ -78,18 +78,8 @@ extension InstanceService {
networkDate: response.networkDate networkDate: response.networkDate
) )
// update relationship // update instance
let request = MastodonAuthentication.sortedFetchRequest AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
} }
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.tryMap { result in .tryMap { result in
@ -116,18 +106,8 @@ extension InstanceService {
) )
) )
// update relationship // update instance
let request = MastodonAuthentication.sortedFetchRequest AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
request.predicate = MastodonAuthentication.predicate(domain: domain)
request.returnsObjectsAsFaults = false
do {
let authentications = try managedObjectContext.fetch(request)
for authentication in authentications {
authentication.update(instance: instance)
}
} catch {
assertionFailure(error.localizedDescription)
}
} }
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.tryMap { result in .tryMap { result in

View File

@ -41,7 +41,7 @@ public final class NotificationService {
self.apiService = apiService self.apiService = apiService
self.authenticationService = authenticationService self.authenticationService = authenticationService
authenticationService.$mastodonAuthentications AuthenticationServiceProvider.shared.$authentications
.sink(receiveValue: { [weak self] mastodonAuthentications in .sink(receiveValue: { [weak self] mastodonAuthentications in
guard let self = self else { return } guard let self = self else { return }
@ -100,13 +100,13 @@ extension NotificationService {
let managedObjectContext = authenticationService.managedObjectContext let managedObjectContext = authenticationService.managedObjectContext
return try await managedObjectContext.perform { return try await managedObjectContext.perform {
var items: [UIApplicationShortcutItem] = [] var items: [UIApplicationShortcutItem] = []
for object in authenticationService.mastodonAuthentications { for authentication in AuthenticationServiceProvider.shared.authentications {
guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue } guard let user = authentication.user(in: managedObjectContext) else { continue }
let accessToken = authentication.userAccessToken let accessToken = authentication.userAccessToken
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
guard count > 0 else { continue } guard count > 0 else { continue }
let title = "@\(authentication.user.acctWithDomain)" let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let item = UIApplicationShortcutItem( let item = UIApplicationShortcutItem(
@ -201,9 +201,8 @@ extension NotificationService {
let needsCancelSubscription: Bool = try await managedObjectContext.perform { let needsCancelSubscription: Bool = try await managedObjectContext.perform {
// check authentication exists // check authentication exists
let authenticationRequest = MastodonAuthentication.sortedFetchRequest let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == userAccessToken }
authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken) return results.first == nil
return managedObjectContext.safeFetch(authenticationRequest).first == nil
} }
guard needsCancelSubscription else { guard needsCancelSubscription else {
@ -240,22 +239,17 @@ extension NotificationService {
private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? { private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? {
guard let authenticationService = self.authenticationService else { return nil } guard let authenticationService = self.authenticationService else { return nil }
let managedObjectContext = authenticationService.managedObjectContext let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken }
return try await managedObjectContext.perform { guard let authentication = results.first else { return nil }
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken) return MastodonAuthenticationBox(
request.fetchLimit = 1 authentication: authentication,
guard let authentication = managedObjectContext.safeFetch(request).first else { return nil } domain: authentication.domain,
userID: authentication.userID,
return MastodonAuthenticationBox( appAuthorization: .init(accessToken: authentication.appAccessToken),
authenticationRecord: .init(objectID: authentication.objectID), userAuthorization: .init(accessToken: authentication.userAccessToken),
domain: authentication.domain, inMemoryCache: .sharedCache(for: authentication.userAccessToken)
userID: authentication.userID, )
appAuthorization: .init(accessToken: authentication.appAccessToken),
userAuthorization: .init(accessToken: authentication.userAccessToken),
inMemoryCache: .sharedCache(for: authentication.objectID.description)
)
}
} }
} }

View File

@ -0,0 +1,20 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
public extension UserDefaults {
enum Keys {
static let didMigrateAuthenticationsKey = "didMigrateAuthentications"
}
@objc dynamic var didMigrateAuthentications: Bool {
get {
return bool(forKey: Keys.didMigrateAuthenticationsKey)
}
set {
set(newValue, forKey: Keys.didMigrateAuthenticationsKey)
}
}
}

View File

@ -156,7 +156,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
self.visibility = { self.visibility = {
// default private when user locked // default private when user locked
var visibility: Mastodon.Entity.Status.Visibility = { var visibility: Mastodon.Entity.Status.Visibility = {
guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { guard let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else {
return .public return .public
} }
return author.locked ? .private : .public return author.locked ? .private : .public
@ -224,7 +224,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
assertionFailure() assertionFailure()
return return
} }
let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
var mentionAccts: [String] = [] var mentionAccts: [String] = []
if author?.id != status.author.id { if author?.id != status.author.id {
@ -259,9 +259,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
let _configuration: Mastodon.Entity.Instance.Configuration? = { let _configuration: Mastodon.Entity.Instance.Configuration? = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil var configuration: Mastodon.Entity.Instance.Configuration? = nil
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) let authentication = authContext.mastodonAuthenticationBox.authentication
else { return } configuration = authentication.instance(in: context.managedObjectContext)?.configuration
configuration = authentication.instance?.configuration
} }
return configuration return configuration
}() }()
@ -319,7 +318,7 @@ extension ComposeContentViewModel {
$authContext $authContext
.sink { [weak self] authContext in .sink { [weak self] authContext in
guard let self = self else { return } guard let self = self else { return }
guard let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
self.avatarURL = user.avatarImageURL() self.avatarURL = user.avatarImageURL()
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
self.username = user.acctWithDomain self.username = user.acctWithDomain
@ -565,7 +564,7 @@ extension ComposeContentViewModel {
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
} }
guard let author = _author else { guard let author = _author else {
throw AppError.badAuthentication throw AppError.badAuthentication
@ -621,7 +620,7 @@ extension ComposeContentViewModel {
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
} }
guard let author = _author else { guard let author = _author else {
throw AppError.badAuthentication throw AppError.badAuthentication

View File

@ -237,9 +237,8 @@ extension NotificationView.ViewModel {
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) let authentication = authContext.mastodonAuthenticationBox.authentication
else { return } configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
configuration = authentication.instance?.configurationV2
} }
return configuration return configuration
}() }()

View File

@ -686,9 +686,8 @@ extension StatusView.ViewModel {
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) let authentication = authContext.mastodonAuthenticationBox.authentication
else { return } configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
configuration = authentication.instance?.configurationV2
} }
return configuration return configuration
}() }()

View File

@ -126,6 +126,7 @@ public final class RelationshipViewModel {
$me, $me,
relationshipUpdatePublisher relationshipUpdatePublisher
) )
.receive(on: DispatchQueue.main)
.sink { [weak self] user, me, _ in .sink { [weak self] user, me, _ in
guard let self = self else { return } guard let self = self else { return }
self.update(user: user, me: me) self.update(user: user, me: me)

View File

@ -160,8 +160,7 @@ extension ShareViewController {
extension ShareViewController { extension ShareViewController {
private func setupAuthContext() throws -> AuthContext? { private func setupAuthContext() throws -> AuthContext? {
let request = MastodonAuthentication.activeSortedFetchRequest // use active order let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
let _authentication = try context.managedObjectContext.fetch(request).first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
return _authContext return _authContext
} }

View File

@ -83,9 +83,9 @@ private extension FollowersCountWidgetProvider {
} }
guard guard
let desiredAccount = configuration.account ?? authBox.authenticationRecord.object( let desiredAccount = configuration.account ?? authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain )?.acctWithDomain
else { else {
return completion(.unconfigured) return completion(.unconfigured)
} }

View File

@ -86,9 +86,9 @@ private extension MultiFollowersCountWidgetProvider {
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) { if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
desiredAccounts = configuredAccounts desiredAccounts = configuredAccounts
} else if let currentlyLoggedInAccount = authBox.authenticationRecord.object( } else if let currentlyLoggedInAccount = authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain { )?.acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount] desiredAccounts = [currentlyLoggedInAccount]
} else { } else {
return completion(.unconfigured) return completion(.unconfigured)