Begin completely replacing CoreDataStack entities by Mastodon.Entity

This commit is contained in:
Marcus Kida 2023-10-06 16:15:24 +02:00
parent c80a590306
commit ec5ace4601
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
46 changed files with 561 additions and 1579 deletions

View File

@ -6,8 +6,6 @@
// //
import UIKit import UIKit
import CoreDataStack
import class CoreDataStack.Notification
import MastodonCore import MastodonCore
import MastodonSDK import MastodonSDK
import MastodonLocalization import MastodonLocalization
@ -15,7 +13,7 @@ import MastodonLocalization
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserFollowAction( static func responseToUserFollowAction(
dependency: NeedsDependency & AuthContextProvider, dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser> user: Mastodon.Entity.Account
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
@ -31,41 +29,33 @@ extension DataSourceFacade {
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserFollowRequestAction( static func responseToUserFollowRequestAction(
dependency: NeedsDependency & AuthContextProvider, dependency: NeedsDependency & AuthContextProvider,
notification: ManagedObjectRecord<Notification>, notification: Mastodon.Entity.Notification,
query: Mastodon.API.Account.FollowRequestQuery query: Mastodon.API.Account.FollowRequestQuery
) async throws { ) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged() await selectionFeedbackGenerator.selectionChanged()
let managedObjectContext = dependency.context.managedObjectContext let managedObjectContext = dependency.context.managedObjectContext
let _userID: MastodonUser.ID? = try await managedObjectContext.perform { let userID = notification.account.id
guard let notification = notification.object(in: managedObjectContext) else { return nil }
return notification.account.id
}
guard let userID = _userID else { // let state: MastodonFollowRequestState = try await managedObjectContext.perform {
assertionFailure() // guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) }
throw APIService.APIError.implicit(.badRequest) // return notification.followRequestState
} // }
//
// guard state.state == .none else {
// return
// }
let state: MastodonFollowRequestState = try await managedObjectContext.perform { // try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) } // guard let notification = notification.object(in: managedObjectContext) else { return }
return notification.followRequestState // switch query {
} // case .accept:
// notification.transientFollowRequestState = .init(state: .isAccepting)
guard state.state == .none else { // case .reject:
return // notification.transientFollowRequestState = .init(state: .isRejecting)
} // }
// }
try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return }
switch query {
case .accept:
notification.transientFollowRequestState = .init(state: .isAccepting)
case .reject:
notification.transientFollowRequestState = .init(state: .isRejecting)
}
}
do { do {
_ = try await dependency.context.apiService.followRequest( _ = try await dependency.context.apiService.followRequest(
@ -75,22 +65,23 @@ extension DataSourceFacade {
) )
} catch { } catch {
// reset state when failure // reset state when failure
try? await managedObjectContext.performChanges { // try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return } // guard let notification = notification.object(in: managedObjectContext) else { return }
notification.transientFollowRequestState = .init(state: .none) // notification.transientFollowRequestState = .init(state: .none)
} // }
if let error = error as? Mastodon.API.Error { if let error = error as? Mastodon.API.Error {
switch error.httpResponseStatus { switch error.httpResponseStatus {
case .notFound: case .notFound:
let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext break
try await backgroundManagedObjectContext.performChanges { // let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
guard let notification = notification.object(in: backgroundManagedObjectContext) else { return } // try await backgroundManagedObjectContext.performChanges {
for feed in notification.feeds { // guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
backgroundManagedObjectContext.delete(feed) // for feed in notification.feeds {
} // backgroundManagedObjectContext.delete(feed)
backgroundManagedObjectContext.delete(notification) // }
} // backgroundManagedObjectContext.delete(notification)
// }
default: default:
let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert) let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert)
let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
@ -106,38 +97,38 @@ extension DataSourceFacade {
return return
} }
try? await managedObjectContext.performChanges { // try? await managedObjectContext.performChanges {
guard let notification = notification.object(in: managedObjectContext) else { return } // guard let notification = notification.object(in: managedObjectContext) else { return }
switch query { // switch query {
case .accept: // case .accept:
notification.transientFollowRequestState = .init(state: .isAccept) // notification.transientFollowRequestState = .init(state: .isAccept)
case .reject: // case .reject:
// do nothing due to will delete notification // // do nothing due to will delete notification
break // break
} // }
} // }
let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext // let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext
try? await backgroundManagedObjectContext.performChanges { // try? await backgroundManagedObjectContext.performChanges {
guard let notification = notification.object(in: backgroundManagedObjectContext) else { return } // guard let notification = notification.object(in: backgroundManagedObjectContext) else { return }
switch query { // switch query {
case .accept: // case .accept:
notification.followRequestState = .init(state: .isAccept) // notification.followRequestState = .init(state: .isAccept)
case .reject: // case .reject:
// delete notification // // delete notification
for feed in notification.feeds { // for feed in notification.feeds {
backgroundManagedObjectContext.delete(feed) // backgroundManagedObjectContext.delete(feed)
} // }
backgroundManagedObjectContext.delete(notification) // backgroundManagedObjectContext.delete(notification)
} // }
} // }
} // end func } // end func
} }
extension DataSourceFacade { extension DataSourceFacade {
static func responseToShowHideReblogAction( static func responseToShowHideReblogAction(
dependency: NeedsDependency & AuthContextProvider, dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser> user: Mastodon.Entity.Account
) async throws { ) async throws {
_ = try await dependency.context.apiService.toggleShowReblogs( _ = try await dependency.context.apiService.toggleShowReblogs(
for: user, for: user,

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.authentication.user(in: managedObjectContext) else { return } guard let me = authenticationBox.authentication.user 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.authentication.user(in: managedObjectContext) else { return } guard let me = authenticationBox.authentication.user 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.authentication.user(in: managedObjectContext) else { return } guard let me = authenticationBox.authentication.user 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.authentication.user(in: managedObjectContext) else { return } guard let _ = authenticationBox.authentication.user 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

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

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.authentication.user(in: managedObjectContext), me: authContext.mastodonAuthenticationBox.authentication.user,
statusCache: nil, statusCache: nil,
userCache: nil, userCache: nil,
networkDate: Date())) networkDate: Date()))

View File

@ -29,7 +29,6 @@ final class SendPostIntentHandler: NSObject {
// MARK: - SendPostIntentHandling // MARK: - SendPostIntentHandling
extension SendPostIntentHandler: SendPostIntentHandling { extension SendPostIntentHandler: SendPostIntentHandling {
func handle(intent: SendPostIntent) async -> SendPostIntentResponse { func handle(intent: SendPostIntent) async -> SendPostIntentResponse {
guard let content = intent.content else { guard let content = intent.content else {
return SendPostIntentResponse(code: .failure, userActivity: nil) return SendPostIntentResponse(code: .failure, userActivity: nil)
@ -59,7 +58,7 @@ extension SendPostIntentHandler: SendPostIntentHandling {
} }
mastodonAuthentications = [authentication] mastodonAuthentications = [authentication]
} else { } else {
mastodonAuthentications = try accounts.mastodonAuthentication(in: managedObjectContext) mastodonAuthentications = AuthenticationServiceProvider.shared.authentications.sorted(by: { $0.activedAt > $1.activedAt })
} }
let authenticationBoxes = mastodonAuthentications.map { authentication in let authenticationBoxes = mastodonAuthentications.map { authentication in

View File

@ -19,17 +19,15 @@ extension Account {
let accounts: [Account] = try await managedObjectContext.perform { let accounts: [Account] = try await managedObjectContext.perform {
let results = AuthenticationServiceProvider.shared.authentications let results = AuthenticationServiceProvider.shared.authentications
let accounts = results.compactMap { mastodonAuthentication -> Account? in let accounts = results.compactMap { mastodonAuthentication -> Account? in
guard let user = mastodonAuthentication.user(in: managedObjectContext) else { let user = mastodonAuthentication.user
return nil
}
let account = Account( let account = Account(
identifier: mastodonAuthentication.identifier.uuidString, identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback, display: user.displayNameWithFallback,
subtitle: user.acctWithDomain, subtitle: user.acct, // TODO: CD check
image: user.avatarImageURL().flatMap { INImage(url: $0) } image: user.avatarImageURL().flatMap { INImage(url: $0) }
) )
account.name = user.displayNameWithFallback account.name = user.displayNameWithFallback
account.username = user.acctWithDomain account.username = user.acctWithDomainIfMissing(mastodonAuthentication.domain)
return account return account
} }
return accounts return accounts

View File

@ -6,13 +6,12 @@
// //
import Foundation import Foundation
import CoreDataStack
import MastodonSDK import MastodonSDK
public struct MastodonAuthenticationBox: UserIdentifier { public struct MastodonAuthenticationBox: UserIdentifier {
public let authentication: MastodonAuthentication public let authentication: MastodonAuthentication
public let domain: String public let domain: String
public let userID: MastodonUser.ID public let userID: Mastodon.Entity.Account.ID
public let appAuthorization: Mastodon.API.OAuth.Authorization public let appAuthorization: Mastodon.API.OAuth.Authorization
public let userAuthorization: Mastodon.API.OAuth.Authorization public let userAuthorization: Mastodon.API.OAuth.Authorization
public let inMemoryCache: MastodonAccountInMemoryCache public let inMemoryCache: MastodonAccountInMemoryCache
@ -20,7 +19,7 @@ public struct MastodonAuthenticationBox: UserIdentifier {
public init( public init(
authentication: MastodonAuthentication, authentication: MastodonAuthentication,
domain: String, domain: String,
userID: MastodonUser.ID, userID: Mastodon.Entity.Account.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

View File

@ -2,11 +2,11 @@
import Foundation import Foundation
import Combine import Combine
import CoreDataStack
import MastodonSDK import MastodonSDK
import KeychainAccess import KeychainAccess
import MastodonCommon import MastodonCommon
import os.log import os.log
import CoreDataStack
public class AuthenticationServiceProvider: ObservableObject { public class AuthenticationServiceProvider: ObservableObject {
private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication") private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication")
@ -23,15 +23,22 @@ public class AuthenticationServiceProvider: ObservableObject {
} }
} }
func update(instance: Instance, where domain: String) { func update(instance: Mastodon.Entity.Instance, where domain: String) {
authentications = authentications.map { authentication in authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication } guard authentication.domain == domain else { return authentication }
return authentication.updating(instance: instance) return authentication.updating(instance: instance)
} }
} }
func update(instanceV2: Mastodon.Entity.V2.Instance, where domain: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication }
return authentication.updating(instanceV2: instanceV2)
}
}
func delete(authentication: MastodonAuthentication) { func delete(authentication: MastodonAuthentication) {
authentications.removeAll(where: { $0 == authentication }) authentications.removeAll(where: { $0.identifier == authentication.identifier })
} }
func activateAuthentication(in domain: String, for userID: String) { func activateAuthentication(in domain: String, for userID: String) {
@ -69,33 +76,47 @@ public extension AuthenticationServiceProvider {
} }
func migrateLegacyAuthentications(in context: NSManagedObjectContext) { func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
do { Task {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest) do {
let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
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 { var migratedAuthentications = [MastodonAuthentication]()
logger.log(level: .default, "Not all account authentications could be migrated.")
}
self.authentications = migratedAuthentications for auth in legacyAuthentications {
userDefaults.didMigrateAuthentications = true let user = try await Mastodon.API.Account.accountInfo(
} catch { session: URLSession.shared,
userDefaults.didMigrateAuthentications = false domain: auth.domain,
logger.log(level: .error, "Could not migrate legacy authentications") userID: auth.userID,
authorization: .init(accessToken: auth.userAccessToken)
).singleOutput().value
let newAuth = MastodonAuthentication(
user: user,
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
)
migratedAuthentications.append(newAuth)
}
if migratedAuthentications.count != legacyAuthentications.count {
logger.log(level: .default, "Not all account authentications could be migrated.")
}
self.authentications = migratedAuthentications
userDefaults.didMigrateAuthentications = true
} catch {
userDefaults.didMigrateAuthentications = false
logger.log(level: .error, "Could not migrate legacy authentications")
}
} }
} }

View File

@ -6,57 +6,8 @@
// //
import UIKit import UIKit
import CoreDataStack
import MastodonSDK import MastodonSDK
extension Instance {
public var configuration: Mastodon.Entity.Instance.Configuration? {
guard let configurationRaw = configurationRaw else { return nil }
guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.Instance.Configuration.self, from: configurationRaw) else {
return nil
}
return configuration
}
static func encode(configuration: Mastodon.Entity.Instance.Configuration) -> Data? {
return try? JSONEncoder().encode(configuration)
}
}
extension Instance {
public var configurationV2: Mastodon.Entity.V2.Instance.Configuration? {
guard
let configurationRaw = configurationV2Raw,
let configuration = try? JSONDecoder().decode(
Mastodon.Entity.V2.Instance.Configuration.self,
from: configurationRaw
)
else {
return nil
}
return configuration
}
static func encodeV2(configuration: Mastodon.Entity.V2.Instance.Configuration) -> Data? {
return try? JSONEncoder().encode(configuration)
}
}
extension Instance {
public var canFollowTags: Bool {
version?.majorServerVersion(greaterThanOrEquals: 4) ?? false // following Tags is support beginning with Mastodon v4.0.0
}
var isTranslationEnabled: Bool {
if let configuration = configurationV2 {
return configuration.translation?.enabled == true
}
return false
}
}
extension String { extension String {
public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool { public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool {
guard guard

View File

@ -1,12 +1,13 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved. // Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation import Foundation
import CoreDataStack
import MastodonSDK import MastodonSDK
public struct MastodonAuthentication: Codable, Hashable { public struct MastodonAuthentication: Codable {
public typealias ID = UUID public typealias ID = UUID
public private(set) var user: Mastodon.Entity.Account
public private(set) var identifier: ID public private(set) var identifier: ID
public private(set) var domain: String public private(set) var domain: String
public private(set) var username: String public private(set) var username: String
@ -21,13 +22,16 @@ public struct MastodonAuthentication: Codable, Hashable {
public private(set) var activedAt: Date public private(set) var activedAt: Date
public private(set) var userID: String public private(set) var userID: String
public private(set) var instanceObjectIdURI: URL?
public private(set) var instance: Mastodon.Entity.Instance?
public private(set) var instanceV2: Mastodon.Entity.V2.Instance?
internal var persistenceIdentifier: String { internal var persistenceIdentifier: String {
"\(username)@\(domain)" "\(username)@\(domain)"
} }
public static func createFrom( public static func createFrom(
user: Mastodon.Entity.Account,
domain: String, domain: String,
userID: String, userID: String,
username: String, username: String,
@ -38,6 +42,7 @@ public struct MastodonAuthentication: Codable, Hashable {
) -> Self { ) -> Self {
let now = Date() let now = Date()
return MastodonAuthentication( return MastodonAuthentication(
user: user,
identifier: .init(), identifier: .init(),
domain: domain, domain: domain,
username: username, username: username,
@ -49,11 +54,13 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: now, updatedAt: now,
activedAt: now, activedAt: now,
userID: userID, userID: userID,
instanceObjectIdURI: nil instance: nil,
instanceV2: nil
) )
} }
func copy( func copy(
user: Mastodon.Entity.Account? = nil,
identifier: ID? = nil, identifier: ID? = nil,
domain: String? = nil, domain: String? = nil,
username: String? = nil, username: String? = nil,
@ -65,9 +72,11 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: Date? = nil, updatedAt: Date? = nil,
activedAt: Date? = nil, activedAt: Date? = nil,
userID: String? = nil, userID: String? = nil,
instanceObjectIdURI: URL? = nil instance: Mastodon.Entity.Instance? = nil,
instanceV2: Mastodon.Entity.V2.Instance? = nil
) -> Self { ) -> Self {
MastodonAuthentication( MastodonAuthentication(
user: user ?? self.user,
identifier: identifier ?? self.identifier, identifier: identifier ?? self.identifier,
domain: domain ?? self.domain, domain: domain ?? self.domain,
username: username ?? self.username, username: username ?? self.username,
@ -79,26 +88,17 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: updatedAt ?? self.updatedAt, updatedAt: updatedAt ?? self.updatedAt,
activedAt: activedAt ?? self.activedAt, activedAt: activedAt ?? self.activedAt,
userID: userID ?? self.userID, userID: userID ?? self.userID,
instanceObjectIdURI: instanceObjectIdURI ?? self.instanceObjectIdURI instance: instance ?? self.instance,
instanceV2: instanceV2 ?? self.instanceV2
) )
} }
public func instance(in context: NSManagedObjectContext) -> Instance? { func updating(instance: Mastodon.Entity.Instance) -> Self {
guard copy(instance: instance)
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? { func updating(instanceV2: Mastodon.Entity.V2.Instance) -> Self {
let userPredicate = MastodonUser.predicate(domain: domain, id: userID) copy(instanceV2: instanceV2)
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
func updating(instance: Instance) -> Self {
copy(instanceObjectIdURI: instance.objectID.uriRepresentation())
} }
func updating(activatedAt: Date) -> Self { func updating(activatedAt: Date) -> Self {

View File

@ -165,23 +165,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Tag.createOrMerge(
in: managedObjectContext,
context: Persistence.Tag.PersistContext(
domain: domain,
entity: entity,
me: me,
networkDate: response.networkDate
)
)
}
}
return response return response
} // end func } // end func
} }

View File

@ -18,7 +18,6 @@ extension APIService {
let targetUserID: MastodonUser.ID let targetUserID: MastodonUser.ID
let targetUsername: String let targetUsername: String
let isBlocking: Bool let isBlocking: Bool
let isFollowing: Bool
} }
@discardableResult @discardableResult
@ -61,39 +60,25 @@ extension APIService {
} }
public func toggleBlock( public func toggleBlock(
user: ManagedObjectRecord<MastodonUser>, user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let me = authenticationBox.authentication.user
let managedObjectContext = backgroundManagedObjectContext let blockedUsers = try await Mastodon.API.Account.blocks(
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { session: session,
let authentication = authenticationBox.authentication domain: authenticationBox.domain,
sinceID: nil,
limit: nil,
authorization: authenticationBox.userAuthorization
).singleOutput()
guard let blockContext = MastodonBlockContext(
let user = user.object(in: managedObjectContext), sourceUserID: me.id,
let me = authentication.user(in: managedObjectContext) targetUserID: user.id,
else { targetUsername: user.username,
throw APIError.implicit(.badRequest) isBlocking: blockedUsers.value.contains(user)
} )
let isBlocking = user.blockingBy.contains(me)
let isFollowing = user.followingBy.contains(me)
// toggle block state
user.update(isBlocking: !isBlocking, by: me)
// update follow state implicitly
if !isBlocking {
// will do block action. set to unfollow
user.update(isFollowing: false, by: me)
}
return MastodonBlockContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isBlocking: isBlocking,
isFollowing: isFollowing
)
}
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do { do {
@ -118,33 +103,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
switch result {
case .success(let response):
let relationship = response.value
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: relationship,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isBlocking: blockContext.isBlocking, by: me)
user.update(isFollowing: blockContext.isFollowing, by: me)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }

View File

@ -19,32 +19,23 @@ extension APIService {
} }
public func bookmark( public func bookmark(
record: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let authentication = authenticationBox.authentication
let me = authentication.user
let managedObjectContext = backgroundManagedObjectContext let isBookmarked = try await Mastodon.API.Bookmarks.bookmarkedStatus(
domain: authenticationBox.domain,
session: session,
authorization: authenticationBox.userAuthorization,
query: .init()
).singleOutput().value.contains(where: { $0.id == status.id }) // TODO: CD is this sufficient? Do we need to check the domain as well?
// update bookmark state and retrieve bookmark context let bookmarkContext = MastodonBookmarkContext(
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { statusID: status.id,
let authentication = authenticationBox.authentication isBookmarked: isBookmarked
)
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let status = _status.reblog ?? _status
let isBookmarked = status.bookmarkedBy.contains(me)
status.update(bookmarked: !isBookmarked, by: me)
let context = MastodonBookmarkContext(
statusID: status.id,
isBookmarked: isBookmarked
)
return context
}
// request bookmark or undo bookmark // request bookmark or undo bookmark
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -61,36 +52,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
// update bookmark state
try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
let status = _status.reblog ?? _status
switch result {
case .success(let response):
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: authenticationBox.domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
case .failure:
// rollback
status.update(bookmarked: bookmarkContext.isBookmarked, by: me)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }
@ -112,34 +73,6 @@ extension APIService {
query: query query: query
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard
let me = authenticationBox.authentication.user(in: managedObjectContext)
else {
assertionFailure()
return
}
for entity in response.value {
let result = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: authenticationBox.domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
result.status.update(bookmarked: true, by: me)
result.status.reblog?.update(bookmarked: true, by: me)
} // end for in
}
return response return response
} // end func } // end func
} }

View File

@ -16,40 +16,24 @@ extension APIService {
private struct MastodonFavoriteContext { private struct MastodonFavoriteContext {
let statusID: Status.ID let statusID: Status.ID
let isFavorited: Bool let isFavorited: Bool
let favoritedCount: Int64 let favoritedCount: Int
} }
public func favorite( public func favorite(
record: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let authentication = authenticationBox.authentication
let me = authentication.user
let managedObjectContext = backgroundManagedObjectContext let _status = status.reblog ?? status
let isFavorited = status.favourited ?? false
// update like state and retrieve like context let favoritedCount = status.favouritesCount
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges { let favoriteContext = MastodonFavoriteContext(
let authentication = authenticationBox.authentication statusID: status.id,
isFavorited: isFavorited,
guard favoritedCount: favoritedCount
let _status = record.object(in: managedObjectContext), )
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let status = _status.reblog ?? _status
let isFavorited = status.favouritedBy.contains(me)
let favoritedCount = status.favouritesCount
let favoriteCount = isFavorited ? favoritedCount - 1 : favoritedCount + 1
status.update(liked: !isFavorited, by: me)
status.update(favouritesCount: favoriteCount)
let context = MastodonFavoriteContext(
statusID: status.id,
isFavorited: isFavorited,
favoritedCount: favoritedCount
)
return context
}
// request like or undo like // request like or undo like
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -66,40 +50,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
// update like state
try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
let status = _status.reblog ?? _status
switch result {
case .success(let response):
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: authenticationBox.domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
if favoriteContext.isFavorited {
status.update(favouritesCount: max(0, status.favouritesCount - 1)) // undo API return count has delay. Needs -1 local
}
case .failure:
// rollback
status.update(liked: favoriteContext.isFavorited, by: me)
status.update(favouritesCount: favoriteContext.favoritedCount)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }
@ -121,73 +71,24 @@ extension APIService {
query: query query: query
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}
for entity in response.value {
let result = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: authenticationBox.domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
result.status.update(liked: true, by: me)
result.status.reblog?.update(liked: true, by: me)
} // end for in
}
return response return response
} // end func } // end func
} }
extension APIService { extension APIService {
public func favoritedBy( public func favoritedBy(
status: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
query: Mastodon.API.Statuses.FavoriteByQuery, query: Mastodon.API.Statuses.FavoriteByQuery,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let _statusID: Status.ID? = try? await managedObjectContext.perform {
guard let _status = status.object(in: managedObjectContext) else { return nil }
let status = _status.reblog ?? _status
return status.id
}
guard let statusID = _statusID else {
throw APIError.implicit(.badRequest)
}
let response = try await Mastodon.API.Statuses.favoriteBy( let response = try await Mastodon.API.Statuses.favoriteBy(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
statusID: statusID, statusID: status.reblog?.id ?? status.id,
query: query, query: query,
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
for entity in response.value {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: .init(
domain: authenticationBox.domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
} // end for in
}
return response return response
} // end func } // end func
} }

View File

@ -17,7 +17,6 @@ extension APIService {
let sourceUserID: MastodonUser.ID let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID let targetUserID: MastodonUser.ID
let isFollowing: Bool let isFollowing: Bool
let isPending: Bool
let needsUnfollow: Bool let needsUnfollow: Bool
} }
@ -30,46 +29,28 @@ extension APIService {
/// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
/// - Returns: publisher for `Relationship` /// - Returns: publisher for `Relationship`
public func toggleFollow( public func toggleFollow(
user: ManagedObjectRecord<MastodonUser>, user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let authentication = authenticationBox.authentication
let me = authentication.user
let managedObjectContext = backgroundManagedObjectContext let otherUser = try await Mastodon.API.Account.followers(
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { session: session,
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } domain: authentication.domain,
guard let user = user.object(in: managedObjectContext) else { return nil } userID: user.id,
query: .init(maxID: nil, limit: nil),
authorization: authenticationBox.userAuthorization
).singleOutput()
let isFollowing = user.followingBy.contains(me) let isFollowing = otherUser.value.contains(me)
let isPending = user.followRequestedBy.contains(me)
let needsUnfollow = isFollowing || isPending
if needsUnfollow { let followContext = MastodonFollowContext(
// unfollow sourceUserID: me.id,
user.update(isFollowing: false, by: me) targetUserID: user.id,
user.update(isFollowRequested: false, by: me) isFollowing: isFollowing,
} else { needsUnfollow: isFollowing
// follow )
if user.locked {
user.update(isFollowing: false, by: me)
user.update(isFollowRequested: true, by: me)
} else {
user.update(isFollowing: true, by: me)
user.update(isFollowRequested: false, by: me)
}
}
let context = MastodonFollowContext(
sourceUserID: me.id,
targetUserID: user.id,
isFollowing: isFollowing,
isPending: isPending,
needsUnfollow: needsUnfollow
)
return context
}
guard let followContext = _followContext else {
throw APIError.implicit(.badRequest)
}
// request follow or unfollow // request follow or unfollow
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
@ -86,48 +67,28 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
// update friendship state
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext),
let user = user.object(in: managedObjectContext)
else { return }
switch result {
case .success(let response):
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isFollowing: followContext.isFollowing, by: me)
user.update(isFollowRequested: followContext.isPending, by: me)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }
public func toggleShowReblogs( public func toggleShowReblogs(
for user: ManagedObjectRecord<MastodonUser>, for user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let me = authenticationBox.authentication.user
let managedObjectContext = backgroundManagedObjectContext let relationship = try await Mastodon.API.Account.relationships(
guard let user = user.object(in: managedObjectContext), session: session,
let me = authenticationBox.authentication.user(in: managedObjectContext) domain: authenticationBox.domain,
else { throw APIError.implicit(.badRequest) } query: .init(ids: [user.id]),
authorization: authenticationBox.userAuthorization
).singleOutput()
let oldShowReblogs = relationship.value.first?.showingReblogs ?? false
let newShowReblogs = !oldShowReblogs
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 newShowReblogs = (oldShowReblogs == false)
do { do {
let response = try await Mastodon.API.Account.follow( let response = try await Mastodon.API.Account.follow(
session: session, session: session,
@ -142,25 +103,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
switch result {
case .success(let response):
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isShowingReblogs: oldShowReblogs, by: me)
}
}
return try result.get() return try result.get()
} }
} }

View File

@ -26,27 +26,6 @@ extension APIService {
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(
domain: authenticationBox.domain,
id: authenticationBox.userID
)
request.fetchLimit = 1
guard let user = managedObjectContext.safeFetch(request).first else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response return response
} }

View File

@ -33,26 +33,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
let user = result.user
me?.update(isFollowing: true, by: user)
}
}
return response return response
} }

View File

@ -34,29 +34,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
if let me = me {
let user = result.user
user.update(isFollowing: true, by: me)
}
}
}
return response return response
} }

View File

@ -42,25 +42,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
}
return response return response
} }

View File

@ -54,71 +54,6 @@ extension APIService {
NotificationCenter.default.post(name: .userFetched, object: nil) NotificationCenter.default.post(name: .userFetched, object: nil)
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}
// persist status
var statuses: [Status] = []
for entity in response.value {
let result = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil, // TODO: add cache
userCache: nil, // TODO: add cache
networkDate: response.networkDate
)
)
statuses.append(result.status)
}
// locate anchor status
let anchorStatus: Status? = {
guard let maxID = maxID else { return nil }
let request = Status.sortedFetchRequest
request.predicate = Status.predicate(domain: domain, id: maxID)
request.fetchLimit = 1
return try? managedObjectContext.fetch(request).first
}()
// update hasMore flag for anchor status
let acct = Feed.Acct.mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID)
if let anchorStatus = anchorStatus,
let feed = anchorStatus.feed(kind: .home, acct: acct) {
feed.update(hasMore: false)
}
// persist Feed relationship
let sortedStatuses = statuses.sorted(by: { $0.createdAt < $1.createdAt })
let oldestStatus = sortedStatuses.first
for status in sortedStatuses {
let _feed = status.feed(kind: .home, acct: acct)
if let feed = _feed {
feed.update(updatedAt: response.networkDate)
} else {
let feedProperty = Feed.Property(
acct: acct,
kind: .home,
hasMore: false,
createdAt: status.createdAt,
updatedAt: response.networkDate
)
let feed = Feed.insert(into: managedObjectContext, property: feedProperty)
status.attach(feed: feed)
// set hasMore on oldest status if is new feed
if status === oldestStatus {
feed.update(hasMore: true)
}
}
}
}
return response return response
} }

View File

@ -14,8 +14,8 @@ import MastodonSDK
extension APIService { extension APIService {
private struct MastodonMuteContext { private struct MastodonMuteContext {
let sourceUserID: MastodonUser.ID let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: MastodonUser.ID let targetUserID: Mastodon.Entity.Account.ID
let targetUsername: String let targetUsername: String
let isMuting: Bool let isMuting: Bool
} }
@ -32,7 +32,6 @@ extension APIService {
limit: Int?, limit: Int?,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let response = try await Mastodon.API.Account.mutes( let response = try await Mastodon.API.Account.mutes(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
@ -41,51 +40,24 @@ extension APIService {
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
let userIDs = response.value.map { $0.id }
let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs)
let fetchRequest = MastodonUser.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.includesPropertyValues = false
try await managedObjectContext.performChanges {
let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser]
for user in users {
user.deleteStatusAndNotificationFeeds(in: managedObjectContext)
}
}
return response return response
} }
public func toggleMute( public func toggleMute(
user: ManagedObjectRecord<MastodonUser>, user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox,
isMuting: Bool
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges { let me = authenticationBox.authentication.user
let authentication = authenticationBox.authentication
guard let muteContext: MastodonMuteContext = MastodonMuteContext(
let user = user.object(in: managedObjectContext), sourceUserID: me.id,
let me = authentication.user(in: managedObjectContext) targetUserID: user.id,
else { targetUsername: user.username,
throw APIError.implicit(.badRequest) isMuting: isMuting
} )
let isMuting = user.mutingBy.contains(me)
// toggle mute state
user.update(isMuting: !isMuting, by: me)
return MastodonMuteContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isMuting: isMuting
)
}
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do { do {
@ -112,28 +84,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let me = authenticationBox.authentication.user(in: managedObjectContext)
else { return }
switch result {
case .success(let response):
let relationship = response.value
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: relationship,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isMuting: muteContext.isMuting, by: me)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }

View File

@ -86,74 +86,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
assertionFailure()
return
}
var notifications: [Notification] = []
for entity in response.value {
let result = Persistence.Notification.createOrMerge(
in: managedObjectContext,
context: Persistence.Notification.PersistContext(
domain: authenticationBox.domain,
entity: entity,
me: me,
networkDate: response.networkDate
)
)
notifications.append(result.notification)
}
// locate anchor notification
let anchorNotification: Notification? = {
guard let maxID = query.maxID else { return nil }
let request = Notification.sortedFetchRequest
request.predicate = Notification.predicate(
domain: authenticationBox.domain,
userID: authenticationBox.userID,
id: maxID
)
request.fetchLimit = 1
return try? managedObjectContext.fetch(request).first
}()
// update hasMore flag for anchor status
let acct = Feed.Acct.mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID)
let kind: Feed.Kind = scope == .everything ? .notificationAll : .notificationMentions
if let anchorNotification = anchorNotification,
let feed = anchorNotification.feed(kind: kind, acct: acct) {
feed.update(hasMore: false)
}
// persist Feed relationship
let sortedNotifications = notifications.sorted(by: { $0.createAt < $1.createAt })
let oldestNotification = sortedNotifications.first
for notification in notifications {
let _feed = notification.feed(kind: kind, acct: acct)
if let feed = _feed {
feed.update(updatedAt: response.networkDate)
} else {
let feedProperty = Feed.Property(
acct: acct,
kind: kind,
hasMore: false,
createdAt: notification.createAt,
updatedAt: response.networkDate
)
let feed = Feed.insert(into: managedObjectContext, property: feedProperty)
notification.attach(feed: feed)
// set hasMore on oldest notification if is new feed
if notification === oldestNotification {
feed.update(hasMore: true)
}
}
}
}
return response return response
} }
} }
@ -174,20 +106,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
_ = Persistence.Notification.createOrMerge(
in: managedObjectContext,
context: Persistence.Notification.PersistContext(
domain: domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response return response
} }

View File

@ -14,39 +14,18 @@ import MastodonSDK
extension APIService { extension APIService {
public func poll( public func poll(
poll: ManagedObjectRecord<Poll>, poll: Mastodon.Entity.Poll,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
let authorization = authenticationBox.userAuthorization let authorization = authenticationBox.userAuthorization
let managedObjectContext = self.backgroundManagedObjectContext
let pollID: Poll.ID = try await managedObjectContext.perform {
guard let poll = poll.object(in: managedObjectContext) else {
throw APIError.implicit(.badRequest)
}
return poll.id
}
let response = try await Mastodon.API.Polls.poll( let response = try await Mastodon.API.Polls.poll(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
pollID: pollID, pollID: poll.id,
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge(
in: managedObjectContext,
context: Persistence.Poll.PersistContext(
domain: authenticationBox.domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response return response
} }
@ -55,41 +34,19 @@ extension APIService {
extension APIService { extension APIService {
public func vote( public func vote(
poll: ManagedObjectRecord<Poll>, poll: Mastodon.Entity.Poll,
choices: [Int], choices: [Int],
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
let managedObjectContext = backgroundManagedObjectContext
let _pollID: Poll.ID? = try await managedObjectContext.perform {
guard let poll = poll.object(in: managedObjectContext) else { return nil }
return poll.id
}
guard let pollID = _pollID else {
throw APIError.implicit(.badRequest)
}
let response = try await Mastodon.API.Polls.vote( let response = try await Mastodon.API.Polls.vote(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
pollID: pollID, pollID: poll.id,
query: Mastodon.API.Polls.VoteQuery(choices: choices), query: Mastodon.API.Polls.VoteQuery(choices: choices),
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Poll.createOrMerge(
in: managedObjectContext,
context: Persistence.Poll.PersistContext(
domain: authenticationBox.domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response return response
} }

View File

@ -27,24 +27,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
}
return response return response
} // end func } // end func

View File

@ -16,40 +16,24 @@ extension APIService {
private struct MastodonReblogContext { private struct MastodonReblogContext {
let statusID: Status.ID let statusID: Status.ID
let isReblogged: Bool let isReblogged: Bool
let rebloggedCount: Int64 let rebloggedCount: Int
} }
public func reblog( public func reblog(
record: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
// update repost state and retrieve repost context let _status = status.reblog ?? status
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { let isReblogged = status.reblogged ?? false
let authentication = authenticationBox.authentication let rebloggedCount = status.reblogsCount
let reblogCount = isReblogged ? rebloggedCount - 1 : rebloggedCount + 1
guard let reblogContext = MastodonReblogContext(
let me = authentication.user(in: managedObjectContext), statusID: status.id,
let _status = record.object(in: managedObjectContext) isReblogged: isReblogged,
else { return nil } rebloggedCount: rebloggedCount
)
let status = _status.reblog ?? _status
let isReblogged = status.rebloggedBy.contains(me)
let rebloggedCount = status.reblogsCount
let reblogCount = isReblogged ? rebloggedCount - 1 : rebloggedCount + 1
status.update(reblogged: !isReblogged, by: me)
status.update(reblogsCount: Int64(max(0, reblogCount)))
let reblogContext = MastodonReblogContext(
statusID: status.id,
isReblogged: isReblogged,
rebloggedCount: rebloggedCount
)
return reblogContext
}
guard let reblogContext = _reblogContext else {
throw APIError.implicit(.badRequest)
}
// request repost or undo repost // request repost or undo repost
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -66,40 +50,6 @@ extension APIService {
result = .failure(error) result = .failure(error)
} }
// update repost state
try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return }
let status = _status.reblog ?? _status
switch result {
case .success(let response):
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: authentication.domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
if reblogContext.isReblogged {
status.update(reblogsCount: max(0, status.reblogsCount - 1)) // undo API return count has delay. Needs -1 local
}
case .failure:
// rollback
status.update(reblogged: reblogContext.isReblogged, by: me)
status.update(reblogsCount: reblogContext.rebloggedCount)
}
}
let response = try result.get() let response = try result.get()
return response return response
} }
@ -108,42 +58,19 @@ extension APIService {
extension APIService { extension APIService {
public func rebloggedBy( public func rebloggedBy(
status: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
query: Mastodon.API.Statuses.RebloggedByQuery, query: Mastodon.API.Statuses.RebloggedByQuery,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let _statusID: Status.ID? = try? await managedObjectContext.perform {
guard let _status = status.object(in: managedObjectContext) else { return nil }
let status = _status.reblog ?? _status
return status.id
}
guard let statusID = _statusID else {
throw APIError.implicit(.badRequest)
}
let response = try await Mastodon.API.Statuses.rebloggedBy( let response = try await Mastodon.API.Statuses.rebloggedBy(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
statusID: statusID, statusID: status.reblog?.id ?? status.id,
query: query, query: query,
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
for entity in response.value {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: .init(
domain: authenticationBox.domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
} // end for in
}
return response return response
} // end func } // end func
} }

View File

@ -14,17 +14,16 @@ import MastodonSDK
extension APIService { extension APIService {
public func relationship( public func relationship(
records: [ManagedObjectRecord<MastodonUser>], accounts: [Mastodon.Entity.Account],
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
let managedObjectContext = backgroundManagedObjectContext let managedObjectContext = backgroundManagedObjectContext
let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform { let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform {
var ids: [MastodonUser.ID] = [] var ids: [MastodonUser.ID] = []
for record in records { for account in accounts {
guard let user = record.object(in: managedObjectContext) else { continue } guard account.id != authenticationBox.userID else { continue }
guard user.id != authenticationBox.userID else { continue } ids.append(account.id)
ids.append(user.id)
} }
guard !ids.isEmpty else { return nil } guard !ids.isEmpty else { return nil }
return Mastodon.API.Account.RelationshipQuery(ids: ids) return Mastodon.API.Account.RelationshipQuery(ids: ids)
@ -40,28 +39,6 @@ extension APIService {
authorization: authenticationBox.userAuthorization authorization: authenticationBox.userAuthorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
// assertionFailure()
return
}
let relationships = response.value
for record in records {
guard let user = record.object(in: managedObjectContext) else { continue }
guard let relationship = relationships.first(where: { $0.id == user.id }) else { continue }
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: relationship,
me: me,
networkDate: response.networkDate
)
)
} // end for in
}
return response return response
} }

View File

@ -25,39 +25,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
// user
for entity in response.value.accounts {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
}
// statuses
for entity in response.value.statuses {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
} // ent try await managedObjectContext.performChanges { }
return response return response
} }

View File

@ -72,31 +72,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
#if !APP_EXTENSION
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
let status = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
Persistence.StatusEdit.createOrMerge(
in: managedObjectContext,
statusEdits: responseHistory.value,
forStatus: status.status
)
}
#endif
return response return response
} }
} }

View File

@ -30,24 +30,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
#if !APP_EXTENSION
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
#endif
return response return response
} }

View File

@ -27,53 +27,22 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: response.value,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
return response return response
} }
public func deleteStatus( public func deleteStatus(
status: ManagedObjectRecord<Status>, status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> { ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let authorization = authenticationBox.userAuthorization let authorization = authenticationBox.userAuthorization
let managedObjectContext = backgroundManagedObjectContext
let _query: Mastodon.API.Statuses.DeleteStatusQuery? = try? await managedObjectContext.perform {
guard let _status = status.object(in: managedObjectContext) else { return nil }
let status = _status.reblog ?? _status
return Mastodon.API.Statuses.DeleteStatusQuery(id: status.id)
}
guard let query = _query else {
throw APIError.implicit(.badRequest)
}
let response = try await Mastodon.API.Statuses.deleteStatus( let response = try await Mastodon.API.Statuses.deleteStatus(
session: session, session: session,
domain: authenticationBox.domain, domain: authenticationBox.domain,
query: query, query: Mastodon.API.Statuses.DeleteStatusQuery(id: status.id),
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
try await managedObjectContext.performChanges {
guard let status = status.object(in: managedObjectContext) else { return }
managedObjectContext.delete(status)
}
return response return response
} }

View File

@ -27,7 +27,7 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) return response
} // end func } // end func
public func followTag( public func followTag(
@ -44,7 +44,7 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) return response
} // end func } // end func
public func unfollowTag( public func unfollowTag(
@ -61,31 +61,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) return response
} // end func } // end func
} }
fileprivate extension APIService {
func persistTag(
from response: Mastodon.Response.Content<Mastodon.Entity.Tag>,
domain: String,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Tag.createOrMerge(
in: managedObjectContext,
context: Persistence.Tag.PersistContext(
domain: domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response
}
}

View File

@ -27,26 +27,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
let value = response.value.ancestors + response.value.descendants
for entity in value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
}
return response return response
} // end func } // end func
} }

View File

@ -43,24 +43,6 @@ extension APIService {
authorization: authorization authorization: authorization
).singleOutput() ).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
_ = Persistence.Status.createOrMerge(
in: managedObjectContext,
context: Persistence.Status.PersistContext(
domain: domain,
entity: entity,
me: me,
statusCache: nil,
userCache: nil,
networkDate: response.networkDate
)
)
}
}
return response return response
} // end func } // end func

View File

@ -1,75 +1,74 @@
////
//// APIService+CoreData+Instance.swift
//// Mastodon
////
//// Created by Cirno MainasuK on 2021-10-9.
////
// //
// APIService+CoreData+Instance.swift //import Foundation
// Mastodon //import CoreData
//import MastodonSDK
// //
// Created by Cirno MainasuK on 2021-10-9. //extension APIService.CoreData {
// //
// static func createOrMergeInstance(
import Foundation // into managedObjectContext: NSManagedObjectContext,
import CoreData // domain: String,
import CoreDataStack // entity: Mastodon.Entity.Instance,
import MastodonSDK // networkDate: Date
// ) -> (instance: Instance, isCreated: Bool) {
extension APIService.CoreData { // // fetch old mastodon user
// let old: Instance? = {
static func createOrMergeInstance( // let request = Instance.sortedFetchRequest
into managedObjectContext: NSManagedObjectContext, // request.predicate = Instance.predicate(domain: domain)
domain: String, // request.fetchLimit = 1
entity: Mastodon.Entity.Instance, // request.returnsObjectsAsFaults = false
networkDate: Date // do {
) -> (instance: Instance, isCreated: Bool) { // return try managedObjectContext.fetch(request).first
// fetch old mastodon user // } catch {
let old: Instance? = { // assertionFailure(error.localizedDescription)
let request = Instance.sortedFetchRequest // return nil
request.predicate = Instance.predicate(domain: domain) // }
request.fetchLimit = 1 // }()
request.returnsObjectsAsFaults = false //
do { // if let old = old {
return try managedObjectContext.fetch(request).first // // merge old
} catch { // APIService.CoreData.merge(
assertionFailure(error.localizedDescription) // instance: old,
return nil // entity: entity,
} // domain: domain,
}() // networkDate: networkDate
// )
if let old = old { // return (old, false)
// merge old // } else {
APIService.CoreData.merge( // let instance = Instance.insert(
instance: old, // into: managedObjectContext,
entity: entity, // property: Instance.Property(domain: domain, version: entity.version)
domain: domain, // )
networkDate: networkDate // let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
) // instance.update(configurationRaw: configurationRaw)
return (old, false) //
} else { // return (instance, true)
let instance = Instance.insert( // }
into: managedObjectContext, // }
property: Instance.Property(domain: domain, version: entity.version) //
) //}
let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) } //
instance.update(configurationRaw: configurationRaw) //extension APIService.CoreData {
//
return (instance, true) // static func merge(
} // instance: Instance,
} // entity: Mastodon.Entity.Instance,
// domain: String,
} // networkDate: Date
// ) {
extension APIService.CoreData { // guard networkDate > instance.updatedAt else { return }
//
static func merge( // let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
instance: Instance, // instance.update(configurationRaw: configurationRaw)
entity: Mastodon.Entity.Instance, // instance.version = entity.version
domain: String, //
networkDate: Date // instance.didUpdate(at: networkDate)
) { // }
guard networkDate > instance.updatedAt else { return } //
//}
let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
instance.update(configurationRaw: configurationRaw)
instance.version = entity.version
instance.didUpdate(at: networkDate)
}
}

View File

@ -1,77 +1,76 @@
import Foundation //import Foundation
import CoreData //import CoreData
import CoreDataStack //import MastodonSDK
import MastodonSDK //
//extension APIService.CoreData {
extension APIService.CoreData { //
// public struct PersistContext {
public struct PersistContext { // public let domain: String
public let domain: String // public let entity: Mastodon.Entity.V2.Instance
public let entity: Mastodon.Entity.V2.Instance // public let networkDate: Date
public let networkDate: Date //
// public init(
public init( // domain: String,
domain: String, // entity: Mastodon.Entity.V2.Instance,
entity: Mastodon.Entity.V2.Instance, // networkDate: Date
networkDate: Date // ) {
) { // self.domain = domain
self.domain = domain // self.entity = entity
self.entity = entity // self.networkDate = networkDate
self.networkDate = networkDate // }
} // }
} //
// static func createOrMergeInstance(
static func createOrMergeInstance( // in managedObjectContext: NSManagedObjectContext,
in managedObjectContext: NSManagedObjectContext, // context: PersistContext
context: PersistContext // ) -> (instance: Instance, isCreated: Bool) {
) -> (instance: Instance, isCreated: Bool) { // // fetch old mastodon user
// fetch old mastodon user // let old: Instance? = {
let old: Instance? = { // let request = Instance.sortedFetchRequest
let request = Instance.sortedFetchRequest // request.predicate = Instance.predicate(domain: context.domain)
request.predicate = Instance.predicate(domain: context.domain) // request.fetchLimit = 1
request.fetchLimit = 1 // request.returnsObjectsAsFaults = false
request.returnsObjectsAsFaults = false // do {
do { // return try managedObjectContext.fetch(request).first
return try managedObjectContext.fetch(request).first // } catch {
} catch { // assertionFailure(error.localizedDescription)
assertionFailure(error.localizedDescription) // return nil
return nil // }
} // }()
}() //
// if let old = old {
if let old = old { // APIService.CoreData.merge(
APIService.CoreData.merge( // instance: old,
instance: old, // context: context
context: context // )
) // return (old, false)
return (old, false) // } else {
} else { // let instance = Instance.insert(
let instance = Instance.insert( // into: managedObjectContext,
into: managedObjectContext, // property: Instance.Property(domain: context.domain, version: context.entity.version)
property: Instance.Property(domain: context.domain, version: context.entity.version) // )
) // let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) } // instance.update(configurationV2Raw: configurationRaw)
instance.update(configurationV2Raw: configurationRaw) //
// return (instance, true)
return (instance, true) // }
} // }
} //
//}
} //
//extension APIService.CoreData {
extension APIService.CoreData { //
// static func merge(
static func merge( // instance: Instance,
instance: Instance, // context: PersistContext
context: PersistContext // ) {
) { // guard context.networkDate > instance.updatedAt else { return }
guard context.networkDate > instance.updatedAt else { return } //
// let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) } // instance.update(configurationV2Raw: configurationRaw)
instance.update(configurationV2Raw: configurationRaw) // instance.version = context.entity.version
instance.version = context.entity.version //
// instance.didUpdate(at: context.networkDate)
instance.didUpdate(at: context.networkDate) // }
} //
//}
}

View File

@ -8,7 +8,6 @@
import Foundation import Foundation
import Combine import Combine
import CoreData import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
public final class InstanceService { public final class InstanceService {
@ -16,7 +15,6 @@ public final class InstanceService {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
let backgroundManagedObjectContext: NSManagedObjectContext
weak var apiService: APIService? weak var apiService: APIService?
weak var authenticationService: AuthenticationService? weak var authenticationService: AuthenticationService?
@ -26,7 +24,6 @@ public final class InstanceService {
apiService: APIService, apiService: APIService,
authenticationService: AuthenticationService authenticationService: AuthenticationService
) { ) {
self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext
self.apiService = apiService self.apiService = apiService
self.authenticationService = authenticationService self.authenticationService = authenticationService
@ -68,56 +65,38 @@ extension InstanceService {
} }
private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) -> AnyPublisher<Void, Error> { private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext return Future<Void, Error> { promise in
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
// update instance // update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain) AuthenticationServiceProvider.shared.update(instance: response.value, where: domain)
} promise(.success(()))
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
} }
// .setFailureType(to: Error.self)
// .tryMap { result in
// switch result {
// case .success:
// break
// case .failure(let error):
// throw error
// }
// }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) -> AnyPublisher<Void, Error> { private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext return Future<Void, Error> { promise in
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
in: managedObjectContext,
context: .init(
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
)
// update instance // update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain) AuthenticationServiceProvider.shared.update(instanceV2: response.value, where: domain)
} promise(.success(()))
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
} }
// .setFailureType(to: Error.self)
// .tryMap { result in
// switch result {
// case .success:
// break
// case .failure(let error):
// throw error
// }
// }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

View File

@ -101,12 +101,12 @@ extension NotificationService {
return try await managedObjectContext.perform { return try await managedObjectContext.perform {
var items: [UIApplicationShortcutItem] = [] var items: [UIApplicationShortcutItem] = []
for authentication in AuthenticationServiceProvider.shared.authentications { for authentication in AuthenticationServiceProvider.shared.authentications {
guard let user = authentication.user(in: managedObjectContext) else { continue } let user = authentication.user
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 = "@\(user.acctWithDomain)" let title = "@\(user.acctWithDomainIfMissing(authentication.domain))"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let item = UIApplicationShortcutItem( let item = UIApplicationShortcutItem(

View File

@ -61,8 +61,8 @@ extension ComposeContentViewModel {
// configure status // configure status
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let replyTo = status.object(in: context.managedObjectContext) else { return }
cell.statusView.configure(status: replyTo) cell.statusView.configure(status: status)
} }
} }
} }

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import Meta import Meta
import MetaTextKit import MetaTextKit
import MastodonMeta import MastodonMeta
@ -23,7 +22,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
public enum ComposeContext { public enum ComposeContext {
case composeStatus case composeStatus
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource) case editStatus(status: Mastodon.Entity.Status, statusSource: Mastodon.Entity.StatusSource)
} }
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
@ -156,31 +155,25 @@ 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.authentication.user(in: context.managedObjectContext) else { let author = authContext.mastodonAuthenticationBox.authentication.user
return .public
}
return author.locked ? .private : .public return author.locked ? .private : .public
}() }()
// set visibility for reply post // set visibility for reply post
if case .reply(let record) = destination { if case .reply(let status) = destination {
context.managedObjectContext.performAndWait { let repliedStatusVisibility = status.visibility
guard let status = record.object(in: context.managedObjectContext) else { switch repliedStatusVisibility {
assertionFailure() case .public, .unlisted:
return // keep default
} break
let repliedStatusVisibility = status.visibility case .private:
switch repliedStatusVisibility { visibility = .private
case .public, .unlisted: case .direct:
// keep default visibility = .direct
break case ._other:
case .private: assertionFailure()
visibility = .private break
case .direct: case .none:
visibility = .direct break
case ._other:
assertionFailure()
break
}
} }
} }
return visibility return visibility
@ -191,7 +184,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
) )
if case let ComposeContext.editStatus(status, _) = composeContext { if case let ComposeContext.editStatus(status, _) = composeContext {
if status.isContentSensitive { if status.sensitive == true {
isContentWarningActive = true isContentWarningActive = true
contentWarning = status.spoilerText ?? "" contentWarning = status.spoilerText ?? ""
} }
@ -201,7 +194,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
if let pollExpiresAt = poll.expiresAt { if let pollExpiresAt = poll.expiresAt {
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt) pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
} }
pollOptions = poll.options.sortedByIndex().map { pollOptions = poll.options.map {
let option = PollComposeItem.Option() let option = PollComposeItem.Option()
option.text = $0.title option.text = $0.title
return option return option
@ -218,52 +211,40 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// setup initial value // setup initial value
let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " " let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " "
switch destination { switch destination {
case .reply(let record): case .reply(let status):
context.managedObjectContext.performAndWait { let author = authContext.mastodonAuthenticationBox.authentication.user
guard let status = record.object(in: context.managedObjectContext) else {
assertionFailure()
return
}
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.account.id {
mentionAccts.append("@" + status.author.acct) mentionAccts.append("@" + status.account.acct)
}
let mentions = status.mentions
.filter { author?.id != $0.id }
for mention in mentions {
let acct = "@" + mention.acct
guard !mentionAccts.contains(acct) else { continue }
mentionAccts.append(acct)
}
for acct in mentionAccts {
UITextChecker.learnWord(acct)
}
if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
self.isContentWarningActive = true
self.contentWarning = spoilerText
}
let initialComposeContent = mentionAccts.joined(separator: " ")
let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " "
self.initialContent = preInsertedContent + initialContentWithSpace
self.content = preInsertedContent + initialContentWithSpace
} }
let mentions = status.mentions?
.filter { author.id != $0.id } ?? []
for mention in mentions {
let acct = "@" + mention.acct
guard !mentionAccts.contains(acct) else { continue }
mentionAccts.append(acct)
}
for acct in mentionAccts {
UITextChecker.learnWord(acct)
}
if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
self.isContentWarningActive = true
self.contentWarning = spoilerText
}
let initialComposeContent = mentionAccts.joined(separator: " ")
let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " "
self.initialContent = preInsertedContent + initialContentWithSpace
self.content = preInsertedContent + initialContentWithSpace
case .topLevel: case .topLevel:
self.initialContent = initialContentWithSpace self.initialContent = initialContentWithSpace
self.content = initialContentWithSpace self.content = initialContentWithSpace
} }
// set limit // set limit
let _configuration: Mastodon.Entity.Instance.Configuration? = { let _configuration: Mastodon.Entity.Instance.Configuration? = authContext.mastodonAuthenticationBox.authentication.instance?.configuration
var configuration: Mastodon.Entity.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configuration
}
return configuration
}()
if let configuration = _configuration { if let configuration = _configuration {
// set character limit // set character limit
if let maxCharacters = configuration.statuses?.maxCharacters { if let maxCharacters = configuration.statuses?.maxCharacters {
@ -288,22 +269,22 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
case .composeStatus: case .composeStatus:
self.isVisibilityButtonEnabled = true self.isVisibilityButtonEnabled = true
case let .editStatus(status, _): case let .editStatus(status, _):
if let visibility = Mastodon.Entity.Status.Visibility(rawValue: status.visibility.rawValue) { if let visibility = status.visibility {
self.visibility = visibility self.visibility = visibility
} }
self.isVisibilityButtonEnabled = false self.isVisibilityButtonEnabled = false
self.attachmentViewModels = status.attachments.compactMap { self.attachmentViewModels = status.mediaAttachments?.compactMap { attachment -> AttachmentViewModel? in
guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil } guard let assetURL = attachment.url, let url = URL(string: assetURL) else { return nil }
let attachmentViewModel = AttachmentViewModel( let attachmentViewModel = AttachmentViewModel(
api: context.apiService, api: context.apiService,
authContext: authContext, authContext: authContext,
input: .mastodonAssetUrl(url, $0.id), input: .mastodonAssetUrl(url, attachment.id),
sizeLimit: sizeLimit, sizeLimit: sizeLimit,
delegate: self delegate: self
) )
attachmentViewModel.caption = $0.altDescription ?? "" attachmentViewModel.caption = attachment.description ?? ""
return attachmentViewModel return attachmentViewModel
} } ?? []
} }
bind() bind()
@ -318,10 +299,10 @@ 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.authentication.user(in: self.context.managedObjectContext) else { return } let user = authContext.mastodonAuthenticationBox.authentication.user
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.acctWithDomainIfMissing(authContext.mastodonAuthenticationBox.domain)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -503,7 +484,7 @@ extension ComposeContentViewModel {
extension ComposeContentViewModel { extension ComposeContentViewModel {
public enum Destination { public enum Destination {
case topLevel case topLevel
case reply(parent: ManagedObjectRecord<Status>) case reply(parent: Mastodon.Entity.Status)
} }
public enum ScrollViewState { public enum ScrollViewState {
@ -562,10 +543,8 @@ extension ComposeContentViewModel {
// author // author
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author: Mastodon.Entity.Account? = authContext.mastodonAuthenticationBox.authentication.user
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
guard let author = _author else { guard let author = _author else {
throw AppError.badAuthentication throw AppError.badAuthentication
} }
@ -618,13 +597,7 @@ extension ComposeContentViewModel {
// author // author
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author = authContext.mastodonAuthenticationBox.authentication.user
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
guard let author = _author else {
throw AppError.badAuthentication
}
// poll // poll
_ = try { _ = try {
@ -645,7 +618,7 @@ extension ComposeContentViewModel {
} }
return MastodonEditStatusPublisher(statusID: status.id, return MastodonEditStatusPublisher(statusID: status.id,
author: author, author: _author,
isContentWarningComposing: isContentWarningActive, isContentWarningComposing: isContentWarningActive,
contentWarning: contentWarning, contentWarning: contentWarning,
content: content, content: content,
@ -817,3 +790,27 @@ extension ComposeContentViewModel: AttachmentViewModelDelegate {
} }
} }
} }
extension Mastodon.Entity.Account {
public var nameMetaContent: MastodonMetaContent? {
do {
let content = MastodonContent(content: displayNameWithFallback, emojis: emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure()
return nil
}
}
public var bioMetaContent: MastodonMetaContent? {
do {
let content = MastodonContent(content: note, emojis: emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure()
return nil
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved. // Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation import Foundation
import CoreData
import CoreDataStack
import MastodonCore import MastodonCore
import MastodonSDK import MastodonSDK
import Combine import Combine
@ -10,8 +8,8 @@ import Combine
public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
// Input // Input
public let statusID: Status.ID public let statusID: Mastodon.Entity.Status.ID
public let author: ManagedObjectRecord<MastodonUser> public let author: Mastodon.Entity.Account
// content warning // content warning
public let isContentWarningComposing: Bool public let isContentWarningComposing: Bool
@ -40,8 +38,8 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
public var reactor: StatusPublisherReactor? public var reactor: StatusPublisherReactor?
public init( public init(
statusID: Status.ID, statusID: Mastodon.Entity.Status.ID,
author: ManagedObjectRecord<MastodonUser>, author: Mastodon.Entity.Account,
isContentWarningComposing: Bool, isContentWarningComposing: Bool,
contentWarning: String, contentWarning: String,
content: String, content: String,

View File

@ -7,8 +7,6 @@
import Foundation import Foundation
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonCore import MastodonCore
import MastodonSDK import MastodonSDK
@ -17,9 +15,9 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
// Input // Input
// author // author
public let author: ManagedObjectRecord<MastodonUser> public let author: Mastodon.Entity.Account
// refer // refer
public let replyTo: ManagedObjectRecord<Status>? public let replyTo: Mastodon.Entity.Status?
// content warning // content warning
public let isContentWarningComposing: Bool public let isContentWarningComposing: Bool
public let contentWarning: String public let contentWarning: String
@ -47,8 +45,8 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
public var reactor: StatusPublisherReactor? public var reactor: StatusPublisherReactor?
public init( public init(
author: ManagedObjectRecord<MastodonUser>, author: Mastodon.Entity.Account,
replyTo: ManagedObjectRecord<Status>?, replyTo: Mastodon.Entity.Status?,
isContentWarningComposing: Bool, isContentWarningComposing: Bool,
contentWarning: String, contentWarning: String,
content: String, content: String,
@ -161,10 +159,7 @@ extension MastodonStatusPublisher: StatusPublisher {
guard pollOptions != nil else { return nil } guard pollOptions != nil else { return nil }
return self.pollExpireConfigurationOption.seconds return self.pollExpireConfigurationOption.seconds
}() }()
let inReplyToID: Mastodon.Entity.Status.ID? = try await api.backgroundManagedObjectContext.perform { let inReplyToID: Mastodon.Entity.Status.ID? = self.replyTo?.id
guard let replyTo = self.replyTo?.object(in: api.backgroundManagedObjectContext) else { return nil }
return replyTo.id
}
let query = Mastodon.API.Statuses.PublishStatusQuery( let query = Mastodon.API.Statuses.PublishStatusQuery(
status: content, status: content,

View File

@ -228,20 +228,7 @@ extension NotificationView.ViewModel {
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = { lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = self?.authContext?.mastodonAuthenticationBox.authentication.instanceV2?.configuration
guard
let self = self,
let context = self.context,
let authContext = self.authContext
else { return nil }
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()
let menuContext = NotificationView.AuthorMenuContext( let menuContext = NotificationView.AuthorMenuContext(
name: name, name: name,

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore import MastodonCore
import MastodonLocalization import MastodonLocalization
@ -18,29 +17,11 @@ import NaturalLanguage
extension StatusView { extension StatusView {
static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue") static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue")
public func configure(feed: Feed) {
switch feed.kind {
case .home:
guard let status = feed.status else {
assertionFailure()
return
}
configure(status: status)
case .notificationAll:
assertionFailure("TODO")
case .notificationMentions:
assertionFailure("TODO")
case .none:
break
}
}
} }
extension StatusView { extension StatusView {
public func configure(status: Status, statusEdit: StatusEdit) { public func configure(status: Mastodon.Entity.Status, statusEdit: Mastodon.Entity.StatusEdit) {
viewModel.objects.insert(status) viewModel.objects.insert(status)
if let reblog = status.reblog { if let reblog = status.reblog {
viewModel.objects.insert(reblog) viewModel.objects.insert(reblog)
@ -66,7 +47,7 @@ extension StatusView {
viewModel.isContentReveal = true viewModel.isContentReveal = true
} }
public func configure(status: Status) { public func configure(status: Mastodon.Entity.Status) {
viewModel.objects.insert(status) viewModel.objects.insert(status)
if let reblog = status.reblog { if let reblog = status.reblog {
viewModel.objects.insert(reblog) viewModel.objects.insert(reblog)
@ -99,7 +80,7 @@ extension StatusView {
} }
extension StatusView { extension StatusView {
private func configureHeader(status: Status) { private func configureHeader(status: Mastodon.Entity.Status) {
if let _ = status.reblog { if let _ = status.reblog {
Publishers.CombineLatest( Publishers.CombineLatest(
status.author.publisher(for: \.displayName), status.author.publisher(for: \.displayName),
@ -189,7 +170,7 @@ extension StatusView {
} }
} }
public func configureAuthor(author: MastodonUser) { public func configureAuthor(author: Mastodon.Entity.Account) {
// author avatar // author avatar
Publishers.CombineLatest( Publishers.CombineLatest(
author.publisher(for: \.avatar), author.publisher(for: \.avatar),
@ -300,7 +281,7 @@ extension StatusView {
configure(status: originalStatus) configure(status: originalStatus)
} }
func configureTranslated(status: Status) { func configureTranslated(status: Mastodon.Entity.Status) {
let translatedContent: Status.TranslatedContent? = { let translatedContent: Status.TranslatedContent? = {
if let translatedContent = status.reblog?.translatedContent { if let translatedContent = status.reblog?.translatedContent {
return translatedContent return translatedContent
@ -330,7 +311,7 @@ extension StatusView {
} }
} }
private func configureContent(statusEdit: StatusEdit, status: Status) { private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: Mastodon.Entity.Status) {
statusEdit.spoilerText.map { statusEdit.spoilerText.map {
viewModel.spoilerContent = PlaintextMetaContent(string: $0) viewModel.spoilerContent = PlaintextMetaContent(string: $0)
} }
@ -350,7 +331,7 @@ extension StatusView {
} }
} }
private func configureContent(status: Status) { private func configureContent(status: Mastodon.Entity.Status) {
guard status.translatedContent == nil else { guard status.translatedContent == nil else {
return configureTranslated(status: status) return configureTranslated(status: status)
} }
@ -404,7 +385,7 @@ extension StatusView {
viewModel.mediaViewConfigurations = configurations viewModel.mediaViewConfigurations = configurations
} }
private func configurePollHistory(statusEdit: StatusEdit) { private func configurePollHistory(statusEdit: Mastodon.Entity.StatusEdit) {
guard let poll = statusEdit.poll else { return } guard let poll = statusEdit.poll else { return }
let pollItems = poll.options.map { PollItem.history(option: $0) } let pollItems = poll.options.map { PollItem.history(option: $0) }
@ -417,7 +398,7 @@ extension StatusView {
pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot) pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
} }
private func configurePoll(status: Status) { private func configurePoll(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status let status = status.reblog ?? status
if let poll = status.poll { if let poll = status.poll {
@ -488,7 +469,7 @@ extension StatusView {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func configureCard(status: Status) { private func configureCard(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status let status = status.reblog ?? status
if viewModel.mediaViewConfigurations.isEmpty { if viewModel.mediaViewConfigurations.isEmpty {
status.publisher(for: \.card) status.publisher(for: \.card)
@ -499,7 +480,7 @@ extension StatusView {
} }
} }
private func configureToolbar(status: Status) { private func configureToolbar(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status let status = status.reblog ?? status
status.publisher(for: \.repliesCount) status.publisher(for: \.repliesCount)
@ -560,7 +541,7 @@ extension StatusView {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func configureFilter(status: Status) { private func configureFilter(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status let status = status.reblog ?? status
let content = status.content.lowercased() let content = status.content.lowercased()

View File

@ -671,7 +671,7 @@ extension StatusView.ViewModel {
publishersTwo.eraseToAnyPublisher(), publishersTwo.eraseToAnyPublisher(),
publishersThree.eraseToAnyPublisher() publishersThree.eraseToAnyPublisher()
).eraseToAnyPublisher() ).eraseToAnyPublisher()
.sink { tupleOne, tupleTwo, tupleThree in .sink { [weak self] tupleOne, tupleTwo, tupleThree in
let (authorName, isMyself) = tupleOne let (authorName, isMyself) = tupleOne
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree let (translatedFromLanguage, language) = tupleThree
@ -681,21 +681,8 @@ extension StatusView.ViewModel {
return return
} }
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = { lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? =
guard self?.authContext?.mastodonAuthenticationBox.authentication.instanceV2?.configuration
let context = self.context,
let authContext = self.authContext
else {
return nil
}
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()
let menuContext = StatusAuthorView.AuthorMenuContext( let menuContext = StatusAuthorView.AuthorMenuContext(
name: name, name: name,

View File

@ -85,13 +85,7 @@ private extension FollowersCountWidgetProvider {
return completion(.unconfigured) return completion(.unconfigured)
} }
guard let desiredAccount = configuration.account ?? authBox.authentication.user.acctWithDomainIfMissing(authBox.domain)
let desiredAccount = configuration.account ?? authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain
else {
return completion(.unconfigured)
}
guard guard
let resultingAccount = try await WidgetExtension.appContext let resultingAccount = try await WidgetExtension.appContext

View File

@ -86,12 +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.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount]
} else { } else {
return completion(.unconfigured) let currentlyLoggedInAccount = authBox.authentication.user.acctWithDomainIfMissing(authBox.domain)
desiredAccounts = [currentlyLoggedInAccount]
} }
var accounts = [MultiFollowersEntryAccountable]() var accounts = [MultiFollowersEntryAccountable]()