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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,11 @@
import Foundation
import Combine
import CoreDataStack
import MastodonSDK
import KeychainAccess
import MastodonCommon
import os.log
import CoreDataStack
public class AuthenticationServiceProvider: ObservableObject {
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
guard authentication.domain == domain else { return authentication }
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) {
authentications.removeAll(where: { $0 == authentication })
authentications.removeAll(where: { $0.identifier == authentication.identifier })
}
func activateAuthentication(in domain: String, for userID: String) {
@ -69,33 +76,47 @@ public extension AuthenticationServiceProvider {
}
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
do {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in
return MastodonAuthentication(
identifier: auth.identifier,
domain: auth.domain,
username: auth.username,
appAccessToken: auth.appAccessToken,
userAccessToken: auth.userAccessToken,
clientID: auth.clientID,
clientSecret: auth.clientSecret,
createdAt: auth.createdAt,
updatedAt: auth.updatedAt,
activedAt: auth.activedAt,
userID: auth.userID
)
}
Task {
do {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
var migratedAuthentications = [MastodonAuthentication]()
for auth in legacyAuthentications {
let user = try await Mastodon.API.Account.accountInfo(
session: URLSession.shared,
domain: auth.domain,
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.")
}
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")
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 CoreDataStack
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 {
public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool {
guard

View File

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

View File

@ -164,24 +164,7 @@ extension APIService {
query: query,
authorization: authorization
).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
} // end func
}

View File

@ -18,7 +18,6 @@ extension APIService {
let targetUserID: MastodonUser.ID
let targetUsername: String
let isBlocking: Bool
let isFollowing: Bool
}
@discardableResult
@ -61,39 +60,25 @@ extension APIService {
}
public func toggleBlock(
user: ManagedObjectRecord<MastodonUser>,
user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let me = authenticationBox.authentication.user
let managedObjectContext = backgroundManagedObjectContext
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let blockedUsers = try await Mastodon.API.Account.blocks(
session: session,
domain: authenticationBox.domain,
sinceID: nil,
limit: nil,
authorization: authenticationBox.userAuthorization
).singleOutput()
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 blockContext = MastodonBlockContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isBlocking: blockedUsers.value.contains(user)
)
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do {
@ -117,34 +102,7 @@ extension APIService {
} catch {
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()
return response
}

View File

@ -19,32 +19,23 @@ extension APIService {
}
public func bookmark(
record: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let managedObjectContext = backgroundManagedObjectContext
let authentication = authenticationBox.authentication
let me = authentication.user
// update bookmark state and retrieve bookmark context
let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let _status = record.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
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?
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
}
let bookmarkContext = MastodonBookmarkContext(
statusID: status.id,
isBookmarked: isBookmarked
)
// request bookmark or undo bookmark
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -60,37 +51,7 @@ extension APIService {
} catch {
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()
return response
}
@ -111,35 +72,7 @@ extension APIService {
authorization: authenticationBox.userAuthorization,
query: query
).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
} // end func
}

View File

@ -16,40 +16,24 @@ extension APIService {
private struct MastodonFavoriteContext {
let statusID: Status.ID
let isFavorited: Bool
let favoritedCount: Int64
let favoritedCount: Int
}
public func favorite(
record: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let authentication = authenticationBox.authentication
let me = authentication.user
let managedObjectContext = backgroundManagedObjectContext
// update like state and retrieve like context
let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
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
}
let _status = status.reblog ?? status
let isFavorited = status.favourited ?? false
let favoritedCount = status.favouritesCount
let favoriteContext = MastodonFavoriteContext(
statusID: status.id,
isFavorited: isFavorited,
favoritedCount: favoritedCount
)
// request like or undo like
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -66,40 +50,6 @@ extension APIService {
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()
return response
}
@ -120,74 +70,25 @@ extension APIService {
authorization: authenticationBox.userAuthorization,
query: query
).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
} // end func
}
extension APIService {
public func favoritedBy(
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
query: Mastodon.API.Statuses.FavoriteByQuery,
authenticationBox: MastodonAuthenticationBox
) 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(
session: session,
domain: authenticationBox.domain,
statusID: statusID,
statusID: status.reblog?.id ?? status.id,
query: query,
authorization: authenticationBox.userAuthorization
).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
} // end func
}

View File

@ -17,7 +17,6 @@ extension APIService {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let isFollowing: Bool
let isPending: Bool
let needsUnfollow: Bool
}
@ -30,46 +29,28 @@ extension APIService {
/// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
/// - Returns: publisher for `Relationship`
public func toggleFollow(
user: ManagedObjectRecord<MastodonUser>,
user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil }
guard let user = user.object(in: managedObjectContext) else { return nil }
let isFollowing = user.followingBy.contains(me)
let isPending = user.followRequestedBy.contains(me)
let needsUnfollow = isFollowing || isPending
if needsUnfollow {
// unfollow
user.update(isFollowing: false, by: me)
user.update(isFollowRequested: false, by: me)
} else {
// 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
}
let authentication = authenticationBox.authentication
let me = authentication.user
guard let followContext = _followContext else {
throw APIError.implicit(.badRequest)
}
let otherUser = try await Mastodon.API.Account.followers(
session: session,
domain: authentication.domain,
userID: user.id,
query: .init(maxID: nil, limit: nil),
authorization: authenticationBox.userAuthorization
).singleOutput()
let isFollowing = otherUser.value.contains(me)
let followContext = MastodonFollowContext(
sourceUserID: me.id,
targetUserID: user.id,
isFollowing: isFollowing,
needsUnfollow: isFollowing
)
// request follow or unfollow
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
@ -85,49 +66,29 @@ extension APIService {
} catch {
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()
return response
}
public func toggleShowReblogs(
for user: ManagedObjectRecord<MastodonUser>,
for user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext),
let me = authenticationBox.authentication.user(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) }
let me = authenticationBox.authentication.user
let relationship = try await Mastodon.API.Account.relationships(
session: session,
domain: authenticationBox.domain,
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 oldShowReblogs = me.showingReblogsBy.contains(user)
let newShowReblogs = (oldShowReblogs == false)
do {
let response = try await Mastodon.API.Account.follow(
session: session,
@ -142,25 +103,6 @@ extension APIService {
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()
}
}

View File

@ -25,28 +25,7 @@ extension APIService {
query: query,
authorization: authenticationBox.userAuthorization
).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
}

View File

@ -32,27 +32,7 @@ extension APIService {
query: query,
authorization: authorization
).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
}

View File

@ -33,30 +33,7 @@ extension APIService {
query: query,
authorization: authorization
).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
}

View File

@ -41,25 +41,6 @@ extension APIService {
hashtag: hashtag,
authorization: authorization
).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
}

View File

@ -53,72 +53,7 @@ extension APIService {
}
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
}

View File

@ -14,8 +14,8 @@ import MastodonSDK
extension APIService {
private struct MastodonMuteContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: Mastodon.Entity.Account.ID
let targetUsername: String
let isMuting: Bool
}
@ -32,7 +32,6 @@ extension APIService {
limit: Int?,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
let managedObjectContext = backgroundManagedObjectContext
let response = try await Mastodon.API.Account.mutes(
session: session,
domain: authenticationBox.domain,
@ -41,51 +40,24 @@ extension APIService {
authorization: authenticationBox.userAuthorization
).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
}
public func toggleMute(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox,
isMuting: Bool
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
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 me = authenticationBox.authentication.user
let muteContext: MastodonMuteContext = MastodonMuteContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isMuting: isMuting
)
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do {
@ -111,29 +83,7 @@ extension APIService {
} catch {
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()
return response
}

View File

@ -86,74 +86,6 @@ extension APIService {
authorization: authorization
).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
}
}
@ -173,21 +105,7 @@ extension APIService {
notificationID: notificationID,
authorization: authorization
).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
}

View File

@ -14,39 +14,18 @@ import MastodonSDK
extension APIService {
public func poll(
poll: ManagedObjectRecord<Poll>,
poll: Mastodon.Entity.Poll,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Poll> {
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(
session: session,
domain: authenticationBox.domain,
pollID: pollID,
pollID: poll.id,
authorization: authorization
).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
}
@ -55,41 +34,19 @@ extension APIService {
extension APIService {
public func vote(
poll: ManagedObjectRecord<Poll>,
poll: Mastodon.Entity.Poll,
choices: [Int],
authenticationBox: MastodonAuthenticationBox
) 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(
session: session,
domain: authenticationBox.domain,
pollID: pollID,
pollID: poll.id,
query: Mastodon.API.Polls.VoteQuery(choices: choices),
authorization: authenticationBox.userAuthorization
).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
}

View File

@ -27,24 +27,6 @@ extension APIService {
authorization: authorization
).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
} // end func

View File

@ -16,40 +16,24 @@ extension APIService {
private struct MastodonReblogContext {
let statusID: Status.ID
let isReblogged: Bool
let rebloggedCount: Int64
let rebloggedCount: Int
}
public func reblog(
record: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
let managedObjectContext = backgroundManagedObjectContext
// update repost state and retrieve repost context
let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let me = authentication.user(in: managedObjectContext),
let _status = record.object(in: managedObjectContext)
else { return nil }
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)
}
let _status = status.reblog ?? status
let isReblogged = status.reblogged ?? false
let rebloggedCount = status.reblogsCount
let reblogCount = isReblogged ? rebloggedCount - 1 : rebloggedCount + 1
let reblogContext = MastodonReblogContext(
statusID: status.id,
isReblogged: isReblogged,
rebloggedCount: rebloggedCount
)
// request repost or undo repost
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Status>, Error>
@ -65,41 +49,7 @@ extension APIService {
} catch {
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()
return response
}
@ -108,42 +58,19 @@ extension APIService {
extension APIService {
public func rebloggedBy(
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
query: Mastodon.API.Statuses.RebloggedByQuery,
authenticationBox: MastodonAuthenticationBox
) 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(
session: session,
domain: authenticationBox.domain,
statusID: statusID,
statusID: status.reblog?.id ?? status.id,
query: query,
authorization: authenticationBox.userAuthorization
).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
} // end func
}

View File

@ -14,17 +14,16 @@ import MastodonSDK
extension APIService {
public func relationship(
records: [ManagedObjectRecord<MastodonUser>],
accounts: [Mastodon.Entity.Account],
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
let managedObjectContext = backgroundManagedObjectContext
let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform {
var ids: [MastodonUser.ID] = []
for record in records {
guard let user = record.object(in: managedObjectContext) else { continue }
guard user.id != authenticationBox.userID else { continue }
ids.append(user.id)
for account in accounts {
guard account.id != authenticationBox.userID else { continue }
ids.append(account.id)
}
guard !ids.isEmpty else { return nil }
return Mastodon.API.Account.RelationshipQuery(ids: ids)
@ -39,28 +38,6 @@ extension APIService {
query: query,
authorization: authenticationBox.userAuthorization
).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
}

View File

@ -24,40 +24,7 @@ extension APIService {
query: query,
authorization: authorization
).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
}

View File

@ -71,32 +71,7 @@ extension APIService {
domain: domain,
authorization: authorization
).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
}
}

View File

@ -29,25 +29,7 @@ extension APIService {
query: query,
authorization: authorization
).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
}

View File

@ -26,54 +26,23 @@ extension APIService {
statusID: statusID,
authorization: authorization
).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
}
public func deleteStatus(
status: ManagedObjectRecord<Status>,
status: Mastodon.Entity.Status,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
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(
session: session,
domain: authenticationBox.domain,
query: query,
query: Mastodon.API.Statuses.DeleteStatusQuery(id: status.id),
authorization: authorization
).singleOutput()
try await managedObjectContext.performChanges {
guard let status = status.object(in: managedObjectContext) else { return }
managedObjectContext.delete(status)
}
return response
}

View File

@ -27,7 +27,7 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // end func
public func followTag(
@ -44,7 +44,7 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // end func
public func unfollowTag(
@ -61,31 +61,6 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // 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

@ -26,27 +26,7 @@ extension APIService {
statusID: statusID,
authorization: authorization
).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
} // end func
}

View File

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

View File

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

View File

@ -101,12 +101,12 @@ extension NotificationService {
return try await managedObjectContext.perform {
var items: [UIApplicationShortcutItem] = []
for authentication in AuthenticationServiceProvider.shared.authentications {
guard let user = authentication.user(in: managedObjectContext) else { continue }
let user = authentication.user
let accessToken = authentication.userAccessToken
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
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 item = UIApplicationShortcutItem(

View File

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

View File

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

View File

@ -225,23 +225,10 @@ extension NotificationView.ViewModel {
notificationView.menuButton.menu = nil
return
}
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.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
}()
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = self?.authContext?.mastodonAuthenticationBox.authentication.instanceV2?.configuration
let menuContext = NotificationView.AuthorMenuContext(
name: name,

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonCore
import MastodonLocalization
@ -18,29 +17,11 @@ import NaturalLanguage
extension StatusView {
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 {
public func configure(status: Status, statusEdit: StatusEdit) {
public func configure(status: Mastodon.Entity.Status, statusEdit: Mastodon.Entity.StatusEdit) {
viewModel.objects.insert(status)
if let reblog = status.reblog {
viewModel.objects.insert(reblog)
@ -66,7 +47,7 @@ extension StatusView {
viewModel.isContentReveal = true
}
public func configure(status: Status) {
public func configure(status: Mastodon.Entity.Status) {
viewModel.objects.insert(status)
if let reblog = status.reblog {
viewModel.objects.insert(reblog)
@ -99,7 +80,7 @@ extension StatusView {
}
extension StatusView {
private func configureHeader(status: Status) {
private func configureHeader(status: Mastodon.Entity.Status) {
if let _ = status.reblog {
Publishers.CombineLatest(
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
Publishers.CombineLatest(
author.publisher(for: \.avatar),
@ -300,7 +281,7 @@ extension StatusView {
configure(status: originalStatus)
}
func configureTranslated(status: Status) {
func configureTranslated(status: Mastodon.Entity.Status) {
let translatedContent: Status.TranslatedContent? = {
if let translatedContent = status.reblog?.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 {
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 {
return configureTranslated(status: status)
}
@ -404,7 +385,7 @@ extension StatusView {
viewModel.mediaViewConfigurations = configurations
}
private func configurePollHistory(statusEdit: StatusEdit) {
private func configurePollHistory(statusEdit: Mastodon.Entity.StatusEdit) {
guard let poll = statusEdit.poll else { return }
let pollItems = poll.options.map { PollItem.history(option: $0) }
@ -417,7 +398,7 @@ extension StatusView {
pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
}
private func configurePoll(status: Status) {
private func configurePoll(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status
if let poll = status.poll {
@ -488,7 +469,7 @@ extension StatusView {
.store(in: &disposeBag)
}
private func configureCard(status: Status) {
private func configureCard(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status
if viewModel.mediaViewConfigurations.isEmpty {
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
status.publisher(for: \.repliesCount)
@ -560,7 +541,7 @@ extension StatusView {
.store(in: &disposeBag)
}
private func configureFilter(status: Status) {
private func configureFilter(status: Mastodon.Entity.Status) {
let status = status.reblog ?? status
let content = status.content.lowercased()

View File

@ -671,7 +671,7 @@ extension StatusView.ViewModel {
publishersTwo.eraseToAnyPublisher(),
publishersThree.eraseToAnyPublisher()
).eraseToAnyPublisher()
.sink { tupleOne, tupleTwo, tupleThree in
.sink { [weak self] tupleOne, tupleTwo, tupleThree in
let (authorName, isMyself) = tupleOne
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree
@ -681,22 +681,9 @@ extension StatusView.ViewModel {
return
}
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = {
guard
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
}()
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? =
self?.authContext?.mastodonAuthenticationBox.authentication.instanceV2?.configuration
let menuContext = StatusAuthorView.AuthorMenuContext(
name: name,
isMuting: isMuting,

View File

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

View File

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