Continue refactoring of MastodonUser and Status (IOS-176, IOS-189)

This commit is contained in:
Marcus Kida 2023-11-17 13:59:22 +01:00
parent 36091e9628
commit b00877d5da
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
47 changed files with 538 additions and 775 deletions

View File

@ -6,8 +6,6 @@
// //
import Combine import Combine
import CoreData
import CoreDataStack
import Foundation import Foundation
import MastodonSDK import MastodonSDK
import UIKit import UIKit
@ -48,12 +46,11 @@ extension ReportSection {
case .status(let record): case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure( configure(
context: context, context: context,
tableView: tableView, tableView: tableView,
cell: cell, cell: cell,
viewModel: .init(value: status), viewModel: .init(value: record),
configuration: configuration configuration: configuration
) )
} }
@ -78,8 +75,7 @@ extension ReportSection {
case .result(let record): case .result(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return } cell.avatarImageView.configure(configuration: .init(url: record.avatarImageURL()))
cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL()))
} }
return cell return cell
case .bottomLoader: case .bottomLoader:

View File

@ -6,32 +6,10 @@
// //
import UIKit import UIKit
import CoreDataStack
import MastodonCore import MastodonCore
import MastodonSDK import MastodonSDK
extension DataSourceFacade { extension DataSourceFacade {
static func responseToUserBlockAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
let apiService = dependency.context.apiService
let authBox = dependency.authContext.mastodonAuthenticationBox
_ = try await apiService.toggleBlock(
user: user,
authenticationBox: authBox
)
try await dependency.context.apiService.getBlocked(
authenticationBox: authBox
)
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
}
static func responseToUserBlockAction( static func responseToUserBlockAction(
dependency: NeedsDependency & AuthContextProvider, dependency: NeedsDependency & AuthContextProvider,
user: Mastodon.Entity.Account user: Mastodon.Entity.Account

View File

@ -6,7 +6,6 @@
// //
import UIKit import UIKit
import CoreDataStack
import MastodonUI import MastodonUI
import MastodonLocalization import MastodonLocalization
import MastodonSDK import MastodonSDK
@ -66,10 +65,8 @@ extension DataSourceFacade {
previewContext: AttachmentPreviewContext previewContext: AttachmentPreviewContext
) async throws { ) async throws {
let managedObjectContext = dependency.context.managedObjectContext let managedObjectContext = dependency.context.managedObjectContext
let attachments: [MastodonAttachment] = try await managedObjectContext.perform { let status = status.reblog ?? status
let status = status.reblog ?? status let attachments = status.mastodonAttachments
return status.mastodonAttachments
}
let thumbnails = await previewContext.thumbnails() let thumbnails = await previewContext.thumbnails()
@ -150,20 +147,14 @@ extension DataSourceFacade {
@MainActor @MainActor
static func coordinateToMediaPreviewScene( static func coordinateToMediaPreviewScene(
dependency: NeedsDependency & MediaPreviewableViewController, dependency: NeedsDependency & MediaPreviewableViewController,
user: ManagedObjectRecord<MastodonUser>, user: Mastodon.Entity.Account,
previewContext: ImagePreviewContext previewContext: ImagePreviewContext
) async throws { ) async throws {
let managedObjectContext = dependency.context.managedObjectContext let managedObjectContext = dependency.context.managedObjectContext
var _avatarAssetURL: String? var _avatarAssetURL: String? = user.avatar
var _headerAssetURL: String? var _headerAssetURL: String? = user.header
try await managedObjectContext.perform {
guard let user = user.object(in: managedObjectContext) else { return }
_avatarAssetURL = user.avatar
_headerAssetURL = user.header
}
let thumbnail = await previewContext.thumbnail() let thumbnail = await previewContext.thumbnail()
let source: MediaPreviewTransitionItem.Source = { let source: MediaPreviewTransitionItem.Source = {

View File

@ -6,7 +6,6 @@
// //
import Foundation import Foundation
import CoreDataStack
import MastodonCore import MastodonCore
import UIKit import UIKit
@ -86,21 +85,10 @@ extension DataSourceFacade {
provider: DataSourceProvider & AuthContextProvider provider: DataSourceProvider & AuthContextProvider
) async throws { ) async throws {
let authenticationBox = provider.authContext.mastodonAuthenticationBox let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges { guard let _ = try? await authenticationBox.authentication.me() else { return }
guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let request = SearchHistory.sortedFetchRequest #warning("re-implement search history")
request.predicate = SearchHistory.predicate(
domain: authenticationBox.domain,
userID: authenticationBox.userID
)
let searchHistories = managedObjectContext.safeFetch(request)
for searchHistory in searchHistories {
managedObjectContext.delete(searchHistory)
}
} // end try await managedObjectContext.performChanges { }
} // end func } // end func
} }

View File

@ -78,12 +78,13 @@ extension AccountListViewModel {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
{ {
AccountListViewModel.configure( Task { @MainActor in
in: managedObjectContext, await AccountListViewModel.configure(
cell: cell, in: managedObjectContext,
authentication: record, cell: cell,
activeAuthentication: activeAuthentication authentication: record,
) activeAuthentication: activeAuthentication
)}
} }
return cell return cell
case .addAccount: case .addAccount:
@ -97,13 +98,14 @@ extension AccountListViewModel {
diffableDataSource?.apply(snapshot) diffableDataSource?.apply(snapshot)
} }
@MainActor
static func configure( static func configure(
in context: NSManagedObjectContext, in context: NSManagedObjectContext,
cell: AccountListTableViewCell, cell: AccountListTableViewCell,
authentication: MastodonAuthentication, authentication: MastodonAuthentication,
activeAuthentication: MastodonAuthentication activeAuthentication: MastodonAuthentication
) { ) async {
guard let user = authentication.user(in: context) else { return } guard let user = try? await authentication.me() else { return }
// avatar // avatar
cell.avatarButton.avatarImageView.configure( cell.avatarButton.avatarImageView.configure(
@ -112,7 +114,7 @@ extension AccountListViewModel {
// name // name
do { do {
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content) let metaContent = try MastodonMetaContent.convert(document: content)
cell.nameLabel.configure(content: metaContent) cell.nameLabel.configure(content: metaContent)
} catch { } catch {

View File

@ -8,8 +8,6 @@
import UIKit import UIKit
import AVKit import AVKit
import Combine import Combine
import CoreData
import CoreDataStack
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import AlamofireImage import AlamofireImage
@ -206,24 +204,25 @@ extension HomeTimelineViewController {
viewModel.timelineIsEmpty viewModel.timelineIsEmpty
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isEmpty in .sink { [weak self] isEmpty in
if isEmpty { Task { @MainActor in
self?.showEmptyView() if isEmpty {
self?.showEmptyView()
let userDoesntFollowPeople: Bool
if let managedObjectContext = self?.context.managedObjectContext, let userDoesntFollowPeople: Bool
let authContext = self?.authContext, if let authContext = self?.authContext,
let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){ let me = try? await authContext.mastodonAuthenticationBox.authentication.me() {
userDoesntFollowPeople = me.followersCount == 0 userDoesntFollowPeople = me.followersCount == 0
} else {
userDoesntFollowPeople = true
}
if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel.presentedSuggestions = true
}
} else { } else {
userDoesntFollowPeople = true self?.emptyView.removeFromSuperview()
} }
if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel.presentedSuggestions = true
}
} else {
self?.emptyView.removeFromSuperview()
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -6,8 +6,6 @@
// //
import UIKit import UIKit
import CoreData
import CoreDataStack
import Combine import Combine
import MastodonSDK import MastodonSDK
import MastodonCore import MastodonCore
@ -188,31 +186,36 @@ extension AuthenticationViewModel {
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken) let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
let managedObjectContext = context.backgroundManagedObjectContext let managedObjectContext = context.backgroundManagedObjectContext
#warning("what happens if instancev2 is not reachable (errors out)??")
return context.apiService.accountVerifyCredentials( return context.apiService.accountVerifyCredentials(
domain: info.domain, domain: info.domain,
authorization: authorization authorization: authorization
) )
.tryMap { response -> Mastodon.Response.Content<Mastodon.Entity.Account> in .flatMap { response in
let account = response.value Publishers.CombineLatest3(
let mastodonUserRequest = MastodonUser.sortedFetchRequest Just(response).setFailureType(to: Error.self).eraseToAnyPublisher(),
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) Mastodon.API.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher(),
mastodonUserRequest.fetchLimit = 1 Mastodon.API.V2.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher()
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { ).eraseToAnyPublisher()
throw AuthenticationError.badCredentials }
} .tryMap { (accountResponse, instanceV1Response, instanceV2Response) -> Mastodon.Response.Content<Mastodon.Entity.Account> in
let account = accountResponse.value
let instanceV1 = instanceV1Response.value
let instanceV2 = instanceV2Response.value
AuthenticationServiceProvider.shared AuthenticationServiceProvider.shared
.authentications .authentications
.insert(MastodonAuthentication.createFrom(domain: info.domain, .insert(MastodonAuthentication.createFrom(domain: info.domain,
userID: mastodonUser.id, userID: account.id,
username: mastodonUser.username, username: account.username,
appAccessToken: userToken.accessToken, // TODO: swap app token appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken, userAccessToken: userToken.accessToken,
clientID: info.clientID, clientID: info.clientID,
clientSecret: info.clientSecret), at: 0) clientSecret: info.clientSecret,
instance: instanceV1,
instanceV2: instanceV2), at: 0)
return response return accountResponse
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
import MastodonCore import MastodonCore
@ -19,7 +18,7 @@ final class ProfileAboutViewModel {
// input // input
let context: AppContext let context: AppContext
@Published var user: MastodonUser? @Published var user: Mastodon.Entity.Account?
@Published var isEditing = false @Published var isEditing = false
@Published var accountForEdit: Mastodon.Entity.Account? @Published var accountForEdit: Mastodon.Entity.Account?
@ -28,7 +27,7 @@ final class ProfileAboutViewModel {
let profileInfo = ProfileInfo() let profileInfo = ProfileInfo()
let profileInfoEditing = ProfileInfo() let profileInfoEditing = ProfileInfo()
@Published var fields: [MastodonField] = [] @Published var fields: [Mastodon.Entity.Field] = []
@Published var emojiMeta: MastodonContent.Emojis = [:] @Published var emojiMeta: MastodonContent.Emojis = [:]
@Published var createdAt: Date = Date() @Published var createdAt: Date = Date()
@ -38,18 +37,18 @@ final class ProfileAboutViewModel {
$user $user
.compactMap { $0 } .compactMap { $0 }
.flatMap { $0.publisher(for: \.emojis) } .compactMap { $0.emojis }
.map { $0.asDictionary } .map { $0.asDictionary }
.assign(to: &$emojiMeta) .assign(to: &$emojiMeta)
$user $user
.compactMap { $0 } .compactMap { $0 }
.flatMap { $0.publisher(for: \.fields) } .compactMap { $0.fields }
.assign(to: &$fields) .assign(to: &$fields)
$user $user
.compactMap { $0 } .compactMap { $0 }
.flatMap { $0.publisher(for: \.createdAt) } .compactMap { $0.createdAt }
.assign(to: &$createdAt) .assign(to: &$createdAt)
Publishers.CombineLatest( Publishers.CombineLatest(

View File

@ -6,13 +6,13 @@
// //
import UIKit import UIKit
import CoreDataStack import MastodonSDK
final class FollowedTagsTableViewCell: UITableViewCell { final class FollowedTagsTableViewCell: UITableViewCell {
private var hashtagView: HashtagTimelineHeaderView! private var hashtagView: HashtagTimelineHeaderView!
private let separatorLine = UIView.separatorLine private let separatorLine = UIView.separatorLine
private weak var viewModel: FollowedTagsViewModel? private weak var viewModel: FollowedTagsViewModel?
private weak var hashtag: Tag? private var hashtag: Mastodon.Entity.Tag?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -67,7 +67,7 @@ private extension FollowedTagsTableViewCell {
} }
extension FollowedTagsTableViewCell { extension FollowedTagsTableViewCell {
func populate(with tag: Tag) { func populate(with tag: Mastodon.Entity.Tag) {
self.hashtag = tag self.hashtag = tag
hashtagView.update(HashtagTimelineHeaderView.Data.from(tag)) hashtagView.update(HashtagTimelineHeaderView.Data.from(tag))
} }

View File

@ -18,7 +18,7 @@ extension FollowedTagsViewModel {
} }
enum Item: Hashable { enum Item: Hashable {
case hashtag(Tag) case hashtag(Mastodon.Entity.Tag)
} }
func tableViewDiffableDataSource( func tableViewDiffableDataSource(

View File

@ -8,14 +8,11 @@
import os import os
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore import MastodonCore
final class FollowedTagsViewModel: NSObject { final class FollowedTagsViewModel: NSObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
let fetchedResultsController: FollowedTagsFetchedResultController
private weak var tableView: UITableView? private weak var tableView: UITableView?
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>? var diffableDataSource: UITableViewDiffableDataSource<Section, Item>?
@ -24,22 +21,18 @@ final class FollowedTagsViewModel: NSObject {
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
@Published var records = [Mastodon.Entity.Tag]()
// output // output
let presentHashtagTimeline = PassthroughSubject<HashtagTimelineViewModel, Never>() let presentHashtagTimeline = PassthroughSubject<HashtagTimelineViewModel, Never>()
init(context: AppContext, authContext: AuthContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.fetchedResultsController = FollowedTagsFetchedResultController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme:
)
super.init() super.init()
self.fetchedResultsController $records
.$records
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] records in
guard let self = self else { return } guard let self = self else { return }
@ -71,15 +64,17 @@ extension FollowedTagsViewModel {
} }
} }
func followOrUnfollow(_ tag: Tag) { func followOrUnfollow(_ tag: Mastodon.Entity.Tag) {
Task { @MainActor in Task { @MainActor in
switch tag.following { switch tag.following {
case true: case .none:
break
case .some(true):
_ = try? await context.apiService.unfollowTag( _ = try? await context.apiService.unfollowTag(
for: tag.name, for: tag.name,
authenticationBox: authContext.mastodonAuthenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
case false: case .some(false):
_ = try? await context.apiService.followTag( _ = try? await context.apiService.followTag(
for: tag.name, for: tag.name,
authenticationBox: authContext.mastodonAuthenticationBox authenticationBox: authContext.mastodonAuthenticationBox
@ -94,7 +89,7 @@ extension FollowedTagsViewModel: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
let object = fetchedResultsController.records[indexPath.row] let object = records[indexPath.row]
let hashtagTimelineViewModel = HashtagTimelineViewModel( let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: self.context, context: self.context,

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import PhotosUI import PhotosUI
import AlamofireImage import AlamofireImage
import CropViewController import CropViewController
@ -270,12 +269,11 @@ extension ProfileHeaderViewController {
extension ProfileHeaderViewController: ProfileHeaderViewDelegate { extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
guard let user = viewModel.user else { return } guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task { Task {
try await DataSourceFacade.coordinateToMediaPreviewScene( try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self, dependency: self,
user: record, user: user,
previewContext: DataSourceFacade.ImagePreviewContext( previewContext: DataSourceFacade.ImagePreviewContext(
imageView: button.avatarImageView, imageView: button.avatarImageView,
containerView: .profileAvatar(profileHeaderView) containerView: .profileAvatar(profileHeaderView)
@ -286,12 +284,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
guard let user = viewModel.user else { return } guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task { Task {
try await DataSourceFacade.coordinateToMediaPreviewScene( try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self, dependency: self,
user: record, user: user,
previewContext: DataSourceFacade.ImagePreviewContext( previewContext: DataSourceFacade.ImagePreviewContext(
imageView: imageView, imageView: imageView,
containerView: .profileBanner(profileHeaderView) containerView: .profileBanner(profileHeaderView)

View File

@ -7,7 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import Kanna import Kanna
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
@ -26,7 +25,7 @@ final class ProfileHeaderViewModel {
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
@Published var user: MastodonUser? @Published var user: Mastodon.Entity.Account?
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var isMyself = false @Published var isMyself = false

View File

@ -7,49 +7,33 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack import MastodonSDK
extension ProfileHeaderView { extension ProfileHeaderView {
func configuration(user: MastodonUser) { func configuration(user: Mastodon.Entity.Account) {
// header // header
user.publisher(for: \.header) viewModel.headerImageURL = URL(string: user.header)
.map { _ in user.headerImageURL() }
.assign(to: \.headerImageURL, on: viewModel)
.store(in: &disposeBag)
// avatar // avatar
user.publisher(for: \.avatar) viewModel.avatarImageURL = user.avatarImageURL()
.map { _ in user.avatarImageURL() }
.assign(to: \.avatarImageURL, on: viewModel)
.store(in: &disposeBag)
// emojiMeta // emojiMeta
user.publisher(for: \.emojis) viewModel.emojiMeta = user.emojiMeta
.map { $0.asDictionary }
.assign(to: \.emojiMeta, on: viewModel)
.store(in: &disposeBag)
// name // name
user.publisher(for: \.displayName) viewModel.name = user.displayNameWithFallback
.map { _ in user.displayNameWithFallback }
.assign(to: \.name, on: viewModel)
.store(in: &disposeBag)
// username // username
viewModel.acct = user.acctWithDomain viewModel.acct = user.acctWithDomain
// bio // bio
user.publisher(for: \.note) viewModel.note = user.note
.assign(to: \.note, on: viewModel)
.store(in: &disposeBag)
// dashboard // dashboard
user.publisher(for: \.statusesCount) viewModel.statusesCount = user.statusesCount
.map { Int($0) }
.assign(to: \.statusesCount, on: viewModel) viewModel.followingCount = user.followingCount
.store(in: &disposeBag)
user.publisher(for: \.followingCount) viewModel.followersCount = user.followersCount
.map { Int($0) }
.assign(to: \.followingCount, on: viewModel)
.store(in: &disposeBag)
user.publisher(for: \.followersCount)
.map { Int($0) }
.assign(to: \.followersCount, on: viewModel)
.store(in: &disposeBag)
} }
} }

View File

@ -13,9 +13,9 @@ import MastodonAsset
import MastodonCore import MastodonCore
import MastodonUI import MastodonUI
import MastodonLocalization import MastodonLocalization
import CoreDataStack
import TabBarPager import TabBarPager
import XLPagerTabStrip import XLPagerTabStrip
import MastodonSDK
protocol ProfileViewModelEditable { protocol ProfileViewModelEditable {
var isEdited: Bool { get } var isEdited: Bool { get }
@ -237,7 +237,7 @@ extension ProfileViewController {
items.append(self.favoriteBarButtonItem) items.append(self.favoriteBarButtonItem)
items.append(self.bookmarkBarButtonItem) items.append(self.bookmarkBarButtonItem)
if self.currentInstance?.canFollowTags == true { if self.currentInstance?.version?.majorServerVersion(greaterThanOrEquals: 4) ?? false == true {
items.append(self.followedTagsBarButtonItem) items.append(self.followedTagsBarButtonItem)
} }
@ -400,7 +400,6 @@ extension ProfileViewController {
return nil return nil
} }
let name = user.displayNameWithFallback let name = user.displayNameWithFallback
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
var menuActions: [MastodonMenu.Action] = [ var menuActions: [MastodonMenu.Action] = [
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
@ -408,9 +407,14 @@ extension ProfileViewController {
.reportUser(.init(name: name)), .reportUser(.init(name: name)),
.shareUser(.init(name: name)), .shareUser(.init(name: name)),
] ]
let relationship = try await context.apiService.relationship(
forAccounts: [user],
authenticationBox: authContext.mastodonAuthenticationBox
).value.first
if let me = self.viewModel?.me, me.following.contains(user) { if let relationship, relationship.following {
let showReblogs = me.showingReblogsBy.contains(user) let showReblogs = relationship.showingReblogs == true
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
menuActions.insert(.hideReblogs(context), at: 1) menuActions.insert(.hideReblogs(context), at: 1)
} }
@ -525,11 +529,10 @@ extension ProfileViewController {
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) { @objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let user = viewModel.user else { return } guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task { Task {
let _activityViewController = try await DataSourceFacade.createActivityViewController( let _activityViewController = try await DataSourceFacade.createActivityViewController(
dependency: self, dependency: self,
user: record user: user
) )
guard let activityViewController = _activityViewController else { return } guard let activityViewController = _activityViewController else { return }
_ = self.coordinator.present( _ = self.coordinator.present(
@ -799,11 +802,10 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
break break
case .follow, .request, .pending, .following: case .follow, .request, .pending, .following:
guard let user = viewModel.user else { return } guard let user = viewModel.user else { return }
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
Task { Task {
try await DataSourceFacade.responseToUserFollowAction( try await DataSourceFacade.responseToUserFollowAction(
dependency: self, dependency: self,
user: record user: user
) )
} }
case .muting: case .muting:
@ -815,13 +817,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
preferredStyle: .alert preferredStyle: .alert
) )
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
Task { Task {
try await DataSourceFacade.responseToUserMuteAction( try await DataSourceFacade.responseToUserMuteAction(
dependency: self, dependency: self,
user: record user: user
) )
} }
} }
@ -838,13 +839,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name),
preferredStyle: .alert preferredStyle: .alert
) )
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
Task { Task {
try await DataSourceFacade.responseToUserBlockAction( try await DataSourceFacade.responseToUserBlockAction(
dependency: self, dependency: self,
user: record user: user
) )
} }
} }
@ -886,14 +886,12 @@ extension ProfileViewController: MastodonMenuDelegate {
func menuAction(_ action: MastodonMenu.Action) { func menuAction(_ action: MastodonMenu.Action) {
guard let user = viewModel.user else { return } guard let user = viewModel.user else { return }
let userRecord: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task { Task {
try await DataSourceFacade.responseToMenuAction( try await DataSourceFacade.responseToMenuAction(
dependency: self, dependency: self,
action: action, action: action,
menuContext: DataSourceFacade.MenuContext( menuContext: DataSourceFacade.MenuContext(
author: userRecord, author: user,
statusViewModel: nil, statusViewModel: nil,
button: nil, button: nil,
barButtonItem: self.moreMenuBarButtonItem barButtonItem: self.moreMenuBarButtonItem
@ -936,7 +934,7 @@ extension ProfileViewController: PagerTabStripNavigateable {
} }
private extension ProfileViewController { private extension ProfileViewController {
var currentInstance: Instance? { var currentInstance: Mastodon.Entity.V2.Instance? {
authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext) authContext.mastodonAuthenticationBox.authentication.instanceV2
} }
} }

View File

@ -7,12 +7,12 @@
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import SafariServices import SafariServices
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
import MastodonLocalization import MastodonLocalization
import MastodonUI import MastodonUI
import MastodonSDK
class MainTabBarController: UITabBarController { class MainTabBarController: UITabBarController {
@ -131,7 +131,6 @@ class MainTabBarController: UITabBarController {
private(set) var isReadyForWizardAvatarButton = false private(set) var isReadyForWizardAvatarButton = false
// output // output
var avatarURLObserver: AnyCancellable?
@Published var avatarURL: URL? @Published var avatarURL: URL?
// haptic feedback // haptic feedback
@ -262,14 +261,8 @@ extension MainTabBarController {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) { if let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount {
self.avatarURLObserver = user.publisher(for: \.avatar) self.avatarURL = user.avatarImageURL()
.sink { [weak self, weak user] _ in
guard let self = self else { return }
guard let user = user else { return }
guard user.managedObjectContext != nil else { return }
self.avatarURL = user.avatarImageURL()
}
// a11y // a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
@ -281,8 +274,6 @@ extension MainTabBarController {
self?.updateUserAccount() self?.updateUserAccount()
} }
.store(in: &self.disposeBag) .store(in: &self.disposeBag)
} else {
self.avatarURLObserver = nil
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -457,16 +448,7 @@ extension MainTabBarController {
authenticationBox: authContext.mastodonAuthenticationBox authenticationBox: authContext.mastodonAuthenticationBox
) )
if let user = authContext.mastodonAuthenticationBox.authentication.user( authContext.mastodonAuthenticationBox.inMemoryCache.meAccount = profileResponse.value
in: context.managedObjectContext
) {
user.update(
property: .init(
entity: profileResponse.value,
domain: authContext.mastodonAuthenticationBox.domain
)
)
}
} }
} }
} }

View File

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

View File

@ -6,9 +6,9 @@
// //
import UIKit import UIKit
import CoreDataStack
import MastodonCore import MastodonCore
import MastodonAsset import MastodonAsset
import MastodonSDK
enum SearchHistorySection: Hashable { enum SearchHistorySection: Hashable {
case main case main
@ -28,20 +28,18 @@ extension SearchHistorySection {
configuration: Configuration configuration: Configuration
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> { ) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, Mastodon.Entity.Account> { cell, indexPath, item in
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let user = item.object(in: context.managedObjectContext) else { return } cell.condensedUserView.configure(with: item)
cell.condensedUserView.configure(with: user)
} }
} }
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, Mastodon.Entity.Tag> { cell, indexPath, item in
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
var contentConfiguration = cell.defaultContentConfiguration() var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.image = UIImage(systemName: "magnifyingglass") contentConfiguration.image = UIImage(systemName: "magnifyingglass")
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
contentConfiguration.text = "#" + hashtag.name contentConfiguration.text = "#" + item.name
cell.contentConfiguration = contentConfiguration cell.contentConfiguration = contentConfiguration
} }

View File

@ -44,13 +44,12 @@ extension SearchResultSection {
case .user(let record): case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
configure( configure(
context: context, context: context,
authContext: authContext, authContext: authContext,
tableView: tableView, tableView: tableView,
cell: cell, cell: cell,
viewModel: UserTableViewCell.ViewModel(user: user, viewModel: UserTableViewCell.ViewModel(user: record,
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
@ -61,12 +60,11 @@ extension SearchResultSection {
case .status(let record): case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
context.managedObjectContext.performAndWait { context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure( configure(
context: context, context: context,
tableView: tableView, tableView: tableView,
cell: cell, cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(status)), viewModel: StatusTableViewCell.ViewModel(value: .status(record)),
configuration: configuration configuration: configuration
) )
} }
@ -126,7 +124,7 @@ extension SearchResultSection {
configuration: Configuration configuration: Configuration
) { ) {
cell.configure( cell.configure(
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), me: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount,
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate delegate: configuration.userTableViewCellDelegate

View File

@ -136,13 +136,13 @@ extension SearchResultViewModel.State {
// reset data source when the search is refresh // reset data source when the search is refresh
if offset == nil { if offset == nil {
viewModel.userFetchedResultsController.userIDs = [] viewModel.statusRecords = []
viewModel.statusFetchedResultsController.statusIDs = [] viewModel.userRecords = []
viewModel.hashtags = [] viewModel.hashtags = []
} }
viewModel.userFetchedResultsController.append(userIDs: userIDs) viewModel.userRecords.append(contentsOf: response.value.accounts)
viewModel.statusFetchedResultsController.append(statusIDs: statusIDs) viewModel.statusRecords.append(contentsOf: response.value.statuses)
var hashtags = viewModel.hashtags var hashtags = viewModel.hashtags
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) { for hashtag in response.value.hashtags where !hashtags.contains(hashtag) {

View File

@ -8,8 +8,6 @@
import Foundation import Foundation
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCore import MastodonCore
import MastodonMeta import MastodonMeta
@ -20,7 +18,7 @@ final class MastodonStatusThreadViewModel {
// input // input
let context: AppContext let context: AppContext
@Published private(set) var deletedObjectIDs: Set<NSManagedObjectID> = Set() @Published private(set) var deletedObjectIDs: Set<Mastodon.Entity.Status.ID> = Set()
// output // output
@Published var __ancestors: [StatusItem] = [] @Published var __ancestors: [StatusItem] = []
@ -41,7 +39,7 @@ final class MastodonStatusThreadViewModel {
let newItems = items.filter { item in let newItems = items.filter { item in
switch item { switch item {
case .thread(let thread): case .thread(let thread):
return !deletedObjectIDs.contains(thread.record.objectID) return !deletedObjectIDs.contains(thread.record.id)
default: default:
assertionFailure() assertionFailure()
return false return false
@ -60,7 +58,7 @@ final class MastodonStatusThreadViewModel {
let newItems = items.filter { item in let newItems = items.filter { item in
switch item { switch item {
case .thread(let thread): case .thread(let thread):
return !deletedObjectIDs.contains(thread.record.objectID) return !deletedObjectIDs.contains(thread.record.id)
default: default:
assertionFailure() assertionFailure()
return false return false
@ -81,14 +79,16 @@ extension MastodonStatusThreadViewModel {
nodes: [Node] nodes: [Node]
) { ) {
let ids = nodes.map { $0.statusID } let ids = nodes.map { $0.statusID }
var dictionary: [Status.ID: Status] = [:] var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:]
do { do {
let request = Status.sortedFetchRequest // let request = Status.sortedFetchRequest
request.predicate = Status.predicate(domain: domain, ids: ids) // request.predicate = Status.predicate(domain: domain, ids: ids)
let statuses = try self.context.managedObjectContext.fetch(request) // let statuses = try self.context.managedObjectContext.fetch(request)
for status in statuses {
dictionary[status.id] = status #warning("figure out what this does")
} // for status in statuses {
// dictionary[status.id] = status
// }
} catch { } catch {
return return
} }
@ -98,9 +98,9 @@ extension MastodonStatusThreadViewModel {
guard let status = dictionary[node.statusID] else { continue } guard let status = dictionary[node.statusID] else { continue }
let isLast = i == nodes.count - 1 let isLast = i == nodes.count - 1
let record = ManagedObjectRecord<Status>(objectID: status.objectID) // let record = ManagedObjectRecord<Status>(objectID: status.objectID)
let context = StatusItem.Thread.Context( let context = StatusItem.Thread.Context(
status: record, status: status,
displayUpperConversationLink: !isLast, displayUpperConversationLink: !isLast,
displayBottomConversationLink: true displayBottomConversationLink: true
) )
@ -119,14 +119,15 @@ extension MastodonStatusThreadViewModel {
let childrenIDs = nodes let childrenIDs = nodes
.map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } }
.flatMap { $0 } .flatMap { $0 }
var dictionary: [Status.ID: Status] = [:] var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:]
do { do {
let request = Status.sortedFetchRequest // let request = Status.sortedFetchRequest
request.predicate = Status.predicate(domain: domain, ids: childrenIDs) // request.predicate = Status.predicate(domain: domain, ids: childrenIDs)
let statuses = try self.context.managedObjectContext.fetch(request) // let statuses = try self.context.managedObjectContext.fetch(request)
for status in statuses { #warning("what is this???")
dictionary[status.id] = status // for status in statuses {
} // dictionary[status.id] = status
// }
} catch { } catch {
return return
} }
@ -135,9 +136,9 @@ extension MastodonStatusThreadViewModel {
for node in nodes { for node in nodes {
guard let status = dictionary[node.statusID] else { continue } guard let status = dictionary[node.statusID] else { continue }
// first tier // first tier
let record = ManagedObjectRecord<Status>(objectID: status.objectID) // let record = ManagedObjectRecord<Status>(objectID: status.objectID)
let context = StatusItem.Thread.Context( let context = StatusItem.Thread.Context(
status: record status: status
) )
let item = StatusItem.thread(.leaf(context: context)) let item = StatusItem.thread(.leaf(context: context))
newItems.append(item) newItems.append(item)
@ -145,9 +146,9 @@ extension MastodonStatusThreadViewModel {
// second tier // second tier
if let child = node.children.first { if let child = node.children.first {
guard let secondaryStatus = dictionary[child.statusID] else { continue } guard let secondaryStatus = dictionary[child.statusID] else { continue }
let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID) // let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
let secondaryContext = StatusItem.Thread.Context( let secondaryContext = StatusItem.Thread.Context(
status: secondaryRecord, status: secondaryStatus,
displayUpperConversationLink: true displayUpperConversationLink: true
) )
let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext))
@ -263,7 +264,7 @@ extension MastodonStatusThreadViewModel.Node {
} }
extension MastodonStatusThreadViewModel { extension MastodonStatusThreadViewModel {
func delete(objectIDs: [NSManagedObjectID]) { func delete(objectIDs: [Mastodon.Entity.Status.ID]) {
var set = deletedObjectIDs var set = deletedObjectIDs
for objectID in objectIDs { for objectID in objectIDs {
set.insert(objectID) set.insert(objectID)

View File

@ -16,24 +16,22 @@ extension Account {
@MainActor @MainActor
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] { static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
// get accounts // get accounts
let accounts: [Account] = try await managedObjectContext.perform { let results = AuthenticationServiceProvider.shared.authentications
let results = AuthenticationServiceProvider.shared.authentications var accounts = [Account]()
let accounts = results.compactMap { mastodonAuthentication -> Account? in for mastodonAuthentication in results {
guard let user = mastodonAuthentication.user(in: managedObjectContext) else { guard let user = try? await mastodonAuthentication.me() else {
return nil continue
}
let account = Account(
identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback,
subtitle: user.acctWithDomain,
image: user.avatarImageURL().flatMap { INImage(url: $0) }
)
account.name = user.displayNameWithFallback
account.username = user.acctWithDomain
return account
} }
return accounts let account = Account(
} // end managedObjectContext.perform identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback,
subtitle: user.acctWithDomain,
image: user.avatarImageURL().flatMap { INImage(url: $0) }
)
account.name = user.displayNameWithFallback
account.username = user.acctWithDomain
accounts.append(account)
}
return accounts return accounts
} }

View File

@ -23,13 +23,20 @@ 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 == authentication })
} }

View File

@ -1,7 +1,6 @@
// 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, Hashable {
@ -21,7 +20,9 @@ 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)"
@ -34,7 +35,9 @@ public struct MastodonAuthentication: Codable, Hashable {
appAccessToken: String, appAccessToken: String,
userAccessToken: String, userAccessToken: String,
clientID: String, clientID: String,
clientSecret: String clientSecret: String,
instance: Mastodon.Entity.Instance?,
instanceV2: Mastodon.Entity.V2.Instance?
) -> Self { ) -> Self {
let now = Date() let now = Date()
return MastodonAuthentication( return MastodonAuthentication(
@ -49,7 +52,8 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: now, updatedAt: now,
activedAt: now, activedAt: now,
userID: userID, userID: userID,
instanceObjectIdURI: nil instance: instance,
instanceV2: instanceV2
) )
} }
@ -65,7 +69,8 @@ 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(
identifier: identifier ?? self.identifier, identifier: identifier ?? self.identifier,
@ -79,31 +84,28 @@ 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,
instanceV2: instanceV2
) )
} }
public func instance(in context: NSManagedObjectContext) -> Instance? {
guard let instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else {
return nil
}
let instance = try? context.existingObject(with: objectID) as? Instance func updating(instance: Mastodon.Entity.Instance) -> Self {
return instance copy(instance: 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 {
copy(activedAt: activatedAt) copy(activedAt: activatedAt)
} }
public func me() async throws -> Mastodon.Entity.Account {
try await Mastodon.API.Account.lookupAccount(
session: .shared, domain: domain,
query: .init(acct: userID),
authorization: Mastodon.API.OAuth.Authorization(accessToken: userAccessToken)
).singleOutput().value
}
} }

View File

@ -7,15 +7,13 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
private struct MastodonBlockContext { private struct MastodonBlockContext {
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 isBlocking: Bool let isBlocking: Bool
let isFollowing: Bool let isFollowing: Bool
@ -41,113 +39,92 @@ extension APIService {
limit: limit, limit: limit,
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 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 managedObjectContext = backgroundManagedObjectContext //// let managedObjectContext = backgroundManagedObjectContext
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { //// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication //// let authentication = authenticationBox.authentication
////
guard // guard
let user = user.object(in: managedObjectContext), //// let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext) // let me = authenticationBox.inMemoryCache.meAccount,
else { // let relationship = try await Mastodon.API.Account.relationships(
throw APIError.implicit(.badRequest) // session: session,
} // domain: authenticationBox.domain,
// query: .init(ids: [user.id]),
let isBlocking = user.blockingBy.contains(me) // authorization: authenticationBox.userAuthorization
let isFollowing = user.followingBy.contains(me) // ).singleOutput().value.first
// toggle block state // else {
user.update(isBlocking: !isBlocking, by: me) // throw APIError.implicit(.badRequest)
// update follow state implicitly // }
if !isBlocking { ////
// will do block action. set to unfollow //// let isBlocking = user.blockingBy.contains(me)
user.update(isFollowing: false, by: me) //// let isFollowing = user.followingBy.contains(me)
} //// // toggle block state
//// user.update(isBlocking: !isBlocking, by: me)
return MastodonBlockContext( //// // update follow state implicitly
sourceUserID: me.id, //// if !isBlocking {
targetUserID: user.id, //// // will do block action. set to unfollow
targetUsername: user.username, //// user.update(isFollowing: false, by: me)
isBlocking: isBlocking, //// }
isFollowing: isFollowing ////
) //// return MastodonBlockContext(
} //// sourceUserID: me.id,
//// targetUserID: user.id,
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> //// targetUsername: user.username,
do { //// isBlocking: isBlocking,
if blockContext.isBlocking { //// isFollowing: isFollowing
let response = try await Mastodon.API.Account.unblock( //// )
session: session, //// }
domain: authenticationBox.domain, //
accountID: blockContext.targetUserID, //
authorization: authenticationBox.userAuthorization //
).singleOutput() // let blockContext = MastodonBlockContext(
result = .success(response) // sourceUserID: me.id,
} else { // targetUserID: user.id,
let response = try await Mastodon.API.Account.block( // targetUsername: user.username,
session: session, // isBlocking: !relationship.blocking,
domain: authenticationBox.domain, // isFollowing: {
accountID: blockContext.targetUserID, // if !relationship.blocking {
authorization: authenticationBox.userAuthorization // return false
).singleOutput() // }
result = .success(response) // return relationship.following
} // }()
} catch { // )
result = .failure(error) //
} // let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
// do {
try await managedObjectContext.performChanges { // if blockContext.isBlocking {
let authentication = authenticationBox.authentication // let response = try await Mastodon.API.Account.unblock(
// session: session,
guard // domain: authenticationBox.domain,
let user = user.object(in: managedObjectContext), // accountID: blockContext.targetUserID,
let me = authentication.user(in: managedObjectContext) // authorization: authenticationBox.userAuthorization
else { return } // ).singleOutput()
// result = .success(response)
// } else {
switch result { // let response = try await Mastodon.API.Account.block(
case .success(let response): // session: session,
let relationship = response.value // domain: authenticationBox.domain,
Persistence.MastodonUser.update( // accountID: blockContext.targetUserID,
mastodonUser: user, // authorization: authenticationBox.userAuthorization
context: Persistence.MastodonUser.RelationshipContext( // ).singleOutput()
entity: relationship, // result = .success(response)
me: me, // }
networkDate: response.networkDate // } catch {
) // result = .failure(error)
) // }
case .failure: //
// rollback // let response = try result.get()
user.update(isBlocking: blockContext.isBlocking, by: me) // return response
user.update(isFollowing: blockContext.isFollowing, by: me) // }
}
}
let response = try result.get()
return response
}
public func toggleBlock( public func toggleBlock(
user: Mastodon.Entity.Account, user: Mastodon.Entity.Account,
@ -178,21 +155,3 @@ extension APIService {
return response return response
} }
} }
extension MastodonUser {
func deleteStatusAndNotificationFeeds(in context: NSManagedObjectContext) {
statuses.map {
$0.feeds
.union($0.reblogFrom.map { $0.feeds }.flatMap { $0 })
.union($0.notifications.map { $0.feeds }.flatMap { $0 })
}
.flatMap { $0 }
.forEach(context.delete)
notifications.map {
$0.feeds
}
.flatMap { $0 }
.forEach(context.delete)
}
}

View File

@ -7,111 +7,109 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
private struct MastodonFollowContext { private struct MastodonFollowContext {
let sourceUserID: MastodonUser.ID let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: MastodonUser.ID let targetUserID: Mastodon.Entity.Account.ID
let isFollowing: Bool let isFollowing: Bool
let isPending: Bool let isPending: Bool
let needsUnfollow: Bool let needsUnfollow: Bool
} }
/// Toggle friendship between target MastodonUser and current MastodonUser // /// Toggle friendship between target MastodonUser and current MastodonUser
/// // ///
/// Following / Following pending <-> Unfollow // /// Following / Following pending <-> Unfollow
/// // ///
/// - Parameters: // /// - Parameters:
/// - mastodonUser: target MastodonUser // /// - mastodonUser: target MastodonUser
/// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` // /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
/// - Returns: publisher for `Relationship` // /// - Returns: publisher for `Relationship`
public func toggleFollow( // public func toggleFollow(
user: ManagedObjectRecord<MastodonUser>, // user: ManagedObjectRecord<MastodonUser>,
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 _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { // let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } // guard let me = authenticationBox.inMemoryCache.meAccount else { return nil }
guard let user = user.object(in: managedObjectContext) else { return nil } // guard let user = user.object(in: managedObjectContext) else { return nil }
//
let isFollowing = user.followingBy.contains(me) // let isFollowing = user.followingBy.contains(me)
let isPending = user.followRequestedBy.contains(me) // let isPending = user.followRequestedBy.contains(me)
let needsUnfollow = isFollowing || isPending // let needsUnfollow = isFollowing || isPending
//
if needsUnfollow { // if needsUnfollow {
// unfollow // // unfollow
user.update(isFollowing: false, by: me) // user.update(isFollowing: false, by: me)
user.update(isFollowRequested: false, by: me) // user.update(isFollowRequested: false, by: me)
} else { // } else {
// follow // // follow
if user.locked { // if user.locked {
user.update(isFollowing: false, by: me) // user.update(isFollowing: false, by: me)
user.update(isFollowRequested: true, by: me) // user.update(isFollowRequested: true, by: me)
} else { // } else {
user.update(isFollowing: true, by: me) // user.update(isFollowing: true, by: me)
user.update(isFollowRequested: false, by: me) // user.update(isFollowRequested: false, by: me)
} // }
} // }
let context = MastodonFollowContext( // let context = MastodonFollowContext(
sourceUserID: me.id, // sourceUserID: me.id,
targetUserID: user.id, // targetUserID: user.id,
isFollowing: isFollowing, // isFollowing: isFollowing,
isPending: isPending, // isPending: isPending,
needsUnfollow: needsUnfollow // needsUnfollow: needsUnfollow
) // )
return context // return context
} // }
//
guard let followContext = _followContext else { // guard let followContext = _followContext else {
throw APIError.implicit(.badRequest) // 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>
do { // do {
let response = try await Mastodon.API.Account.follow( // let response = try await Mastodon.API.Account.follow(
session: session, // session: session,
domain: authenticationBox.domain, // domain: authenticationBox.domain,
accountID: followContext.targetUserID, // accountID: followContext.targetUserID,
followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), // followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()),
authorization: authenticationBox.userAuthorization // authorization: authenticationBox.userAuthorization
).singleOutput() // ).singleOutput()
result = .success(response) // result = .success(response)
} catch { // } catch {
result = .failure(error) // result = .failure(error)
} // }
//
// update friendship state // // update friendship state
try await managedObjectContext.performChanges { // try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext), // guard let me = authenticationBox.authentication.user(in: managedObjectContext),
let user = user.object(in: managedObjectContext) // let user = user.object(in: managedObjectContext)
else { return } // else { return }
//
switch result { // switch result {
case .success(let response): // case .success(let response):
Persistence.MastodonUser.update( // Persistence.MastodonUser.update(
mastodonUser: user, // mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext( // context: Persistence.MastodonUser.RelationshipContext(
entity: response.value, // entity: response.value,
me: me, // me: me,
networkDate: response.networkDate // networkDate: response.networkDate
) // )
) // )
case .failure: // case .failure:
// rollback // // rollback
user.update(isFollowing: followContext.isFollowing, by: me) // user.update(isFollowing: followContext.isFollowing, by: me)
user.update(isFollowRequested: followContext.isPending, 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 toggleFollow( public func toggleFollow(
user: Mastodon.Entity.Account, user: Mastodon.Entity.Account,

View File

@ -7,8 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
@ -25,28 +23,7 @@ extension APIService {
query: query, query: query,
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

@ -7,8 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
@ -32,27 +30,7 @@ extension APIService {
query: query, query: query,
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

@ -7,8 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
@ -33,30 +31,7 @@ extension APIService {
query: query, query: query,
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

@ -7,15 +7,13 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK 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
} }
@ -41,21 +39,6 @@ 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
} }

View File

@ -170,21 +170,7 @@ extension APIService {
notificationID: notificationID, notificationID: notificationID,
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

@ -7,8 +7,6 @@
import Foundation import Foundation
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
@ -27,7 +25,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 +42,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 +59,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

@ -7,8 +7,6 @@
import Foundation import Foundation
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
public final class InstanceService { public final class InstanceService {
@ -16,7 +14,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 +23,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,57 +64,13 @@ 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 AuthenticationServiceProvider.shared.update(instance: response.value, where: domain)
return managedObjectContext.performChanges { return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher()
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
// 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
}
}
.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 AuthenticationServiceProvider.shared.update(instanceV2: response.value, where: domain)
return managedObjectContext.performChanges { return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher()
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
in: managedObjectContext,
context: .init(
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
)
// 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
}
}
.eraseToAnyPublisher()
} }
} }

View File

@ -7,8 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonCommon import MastodonCommon
import MastodonLocalization import MastodonLocalization
@ -96,32 +94,29 @@ extension NotificationService {
extension NotificationService { extension NotificationService {
public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] { public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] {
guard let authenticationService = self.authenticationService else { return [] } // guard let authenticationService = self.authenticationService else { return [] }
let managedObjectContext = authenticationService.managedObjectContext var items: [UIApplicationShortcutItem] = []
return try await managedObjectContext.perform { for authentication in AuthenticationServiceProvider.shared.authentications {
var items: [UIApplicationShortcutItem] = [] guard let user = try? await authentication.me() else { continue }
for authentication in AuthenticationServiceProvider.shared.authentications { let accessToken = authentication.userAccessToken
guard let user = authentication.user(in: managedObjectContext) else { continue } let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
let accessToken = authentication.userAccessToken guard count > 0 else { continue }
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
guard count > 0 else { continue } let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) let item = UIApplicationShortcutItem(
type: NotificationService.unreadShortcutItemIdentifier,
let item = UIApplicationShortcutItem( localizedTitle: title,
type: NotificationService.unreadShortcutItemIdentifier, localizedSubtitle: subtitle,
localizedTitle: title, icon: nil,
localizedSubtitle: subtitle, userInfo: [
icon: nil, "accessToken": accessToken as NSSecureCoding
userInfo: [ ]
"accessToken": accessToken as NSSecureCoding )
] items.append(item)
)
items.append(item)
}
return items
} }
return items
} }
} }

View File

@ -98,6 +98,15 @@ extension Mastodon.Entity.Account {
} }
} }
public var domainFromAcct: String {
if !acct.contains("@") {
return domain!
} else {
let domain = acct.split(separator: "@").last
return String(domain!)
}
}
public func acctWithDomainIfMissing(_ localDomain: String) -> String { public func acctWithDomainIfMissing(_ localDomain: String) -> String {
guard acct.contains("@") else { guard acct.contains("@") else {
return "\(acct)@\(localDomain)" return "\(acct)@\(localDomain)"

View File

@ -172,3 +172,21 @@ extension Mastodon.Entity.Instance.Configuration {
} }
} }
} }
extension Mastodon.Entity.Instance: Hashable {
public static func == (lhs: Mastodon.Entity.Instance, rhs: Mastodon.Entity.Instance) -> Bool {
lhs.uri == rhs.uri
}
public func hash(into hasher: inout Hasher) {
hasher.combine(uri)
}
}
extension Mastodon.Entity.Instance.Configuration: Hashable {
public static func == (lhs: Mastodon.Entity.Instance.Configuration, rhs: Mastodon.Entity.Instance.Configuration) -> Bool {
true
}
public func hash(into hasher: inout Hasher) {}
}

View File

@ -110,3 +110,13 @@ extension Mastodon.Entity.V2.Instance {
public let account: Mastodon.Entity.Account? public let account: Mastodon.Entity.Account?
} }
} }
extension Mastodon.Entity.V2.Instance: Hashable {
public static func == (lhs: Mastodon.Entity.V2.Instance, rhs: Mastodon.Entity.V2.Instance) -> Bool {
lhs.domain == rhs.domain
}
public func hash(into hasher: inout Hasher) {
hasher.combine(domain)
}
}

View File

@ -45,7 +45,6 @@ extension Mastodon.Entity {
case voted case voted
case ownVotes = "own_votes" case ownVotes = "own_votes"
case options case options
case isVoting
} }
} }
} }

View File

@ -73,7 +73,6 @@ extension Mastodon.Entity {
case visibility case visibility
case sensitive case sensitive
case sensitiveToggled
case spoilerText = "spoiler_text" case spoilerText = "spoiler_text"
case mediaAttachments = "media_attachments" case mediaAttachments = "media_attachments"

View File

@ -153,21 +153,17 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
self.authContext = authContext self.authContext = authContext
self.destination = destination self.destination = destination
self.composeContext = composeContext self.composeContext = composeContext
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 { guard let author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else {
return .public 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 status) = destination { if case .reply(let status) = destination {
// context.managedObjectContext.performAndWait {
// guard let status = record.object(in: context.managedObjectContext) else {
// assertionFailure()
// return
// }
let repliedStatusVisibility = status.visibility let repliedStatusVisibility = status.visibility
switch repliedStatusVisibility { switch repliedStatusVisibility {
case .public, .unlisted: case .public, .unlisted:
@ -225,7 +221,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// assertionFailure() // assertionFailure()
// return // return
// } // }
let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) let author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
var mentionAccts: [String] = [] var mentionAccts: [String] = []
if author?.id != status.account.id { if author?.id != status.account.id {
@ -258,11 +254,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// set limit // set limit
let _configuration: Mastodon.Entity.Instance.Configuration? = { let _configuration: Mastodon.Entity.Instance.Configuration? = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil let authentication = authContext.mastodonAuthenticationBox.authentication
context.managedObjectContext.performAndWait { var configuration: Mastodon.Entity.Instance.Configuration? = authentication.instance?.configuration
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configuration
}
return configuration return configuration
}() }()
if let configuration = _configuration { if let configuration = _configuration {
@ -319,7 +312,7 @@ extension ComposeContentViewModel {
$authContext $authContext
.sink { [weak self] authContext in .sink { [weak self] authContext in
guard let self = self else { return } guard let self = self else { return }
guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } guard let user = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else { return }
self.avatarURL = user.avatarImageURL() self.avatarURL = user.avatarImageURL()
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
self.username = user.acctWithDomain self.username = user.acctWithDomain
@ -563,10 +556,7 @@ extension ComposeContentViewModel {
// author // author
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
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
} }
@ -619,10 +609,7 @@ extension ComposeContentViewModel {
// author // author
let managedObjectContext = self.context.managedObjectContext let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>? var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
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
} }
@ -818,3 +805,28 @@ 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
@ -11,7 +9,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
// Input // Input
public let statusID: Mastodon.Entity.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
@ -41,7 +39,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
public init( public init(
statusID: Mastodon.Entity.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,7 +15,7 @@ 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: Mastodon.Entity.Status? public let replyTo: Mastodon.Entity.Status?
// content warning // content warning
@ -47,7 +45,7 @@ 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: Mastodon.Entity.Status?, replyTo: Mastodon.Entity.Status?,
isContentWarningComposing: Bool, isContentWarningComposing: Bool,
contentWarning: String, contentWarning: String,

View File

@ -220,7 +220,7 @@ extension NotificationView.ViewModel {
) )
) )
.sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslatedIsFollowed in .sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslatedIsFollowed in
guard let name = authorName?.string, let self, let context = self.context, let authContext = self.authContext else { guard let name = authorName?.string, let self, let authContext = self.authContext else {
notificationView.menuButton.menu = nil notificationView.menuButton.menu = nil
return return
} }
@ -228,8 +228,7 @@ extension NotificationView.ViewModel {
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
let authentication = authContext.mastodonAuthenticationBox.authentication let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext) let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let menuContext = NotificationView.AuthorMenuContext( let menuContext = NotificationView.AuthorMenuContext(
name: name, name: name,

View File

@ -7,8 +7,6 @@
import UIKit import UIKit
import Combine import Combine
import CoreData
import CoreDataStack
import Meta import Meta
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
@ -668,14 +666,13 @@ extension StatusView.ViewModel {
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree let (translatedFromLanguage, language) = tupleThree
guard let name = authorName?.string, let context = self.context, let authContext = self.authContext else { guard let name = authorName?.string, let authContext = self.authContext else {
statusView.authorView.menuButton.menu = nil statusView.authorView.menuButton.menu = nil
return return
} }
let authentication = authContext.mastodonAuthenticationBox.authentication let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext) let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let menuContext = StatusAuthorView.AuthorMenuContext( let menuContext = StatusAuthorView.AuthorMenuContext(
name: name, name: name,

View File

@ -10,6 +10,11 @@ import Combine
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonSDK import MastodonSDK
import MastodonCore
enum RelationshipError: Error {
case FailedToResolveUser
}
public enum RelationshipAction: Int, CaseIterable { public enum RelationshipAction: Int, CaseIterable {
case showReblogs case showReblogs
@ -127,8 +132,39 @@ public final class RelationshipViewModel {
relationshipUpdatePublisher relationshipUpdatePublisher
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] user, me, _ in .compactMap { user, me, _ -> Optional<(Mastodon.Entity.Account, Mastodon.Entity.Account, MastodonAuthentication)> in
guard let self = self else { return } guard let user, let me else { return nil }
guard let authBox = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first else { return nil }
return (user, me, authBox)
}
.flatMap { (user, me, authBox) in
return Mastodon.API.Account.relationships(
session: .shared,
domain: authBox.domain,
query: .init(ids: [user.id]),
authorization: Mastodon.API.OAuth.Authorization(accessToken: authBox.userAccessToken)
).eraseToAnyPublisher()
}
.sink { completion in
// no-op
} receiveValue: { [weak self] response in
guard let self, let relationship = response.value.first else { return }
isMyself = relationship.id == me?.id
isFollowingBy = relationship.followedBy
isFollowing = relationship.following
isMuting = relationship.muting == true
isBlockingBy = relationship.blockedBy == true
isBlocking = relationship.blocking
showReblogs = relationship.showingReblogs == true
}
.store(in: &disposeBag)
// .sink { [weak self] relationship in
// guard let self = self else { return }
// self.update(user: user, me: me) // self.update(user: user, me: me)
// guard let user = user, let me = me else { // guard let user = user, let me = me else {
@ -149,8 +185,7 @@ public final class RelationshipViewModel {
// guard let self = self else { return } // guard let self = self else { return }
// self.relationshipUpdatePublisher.send() // self.relationshipUpdatePublisher.send()
// } // }
} // }
.store(in: &disposeBag)
} }
} }

View File

@ -82,10 +82,10 @@ private extension FollowersCountWidgetProvider {
return completion(.unconfigured) return completion(.unconfigured)
} }
let meAcctDomain = try? await authBox.authentication.me().acctWithDomain
guard guard
let desiredAccount = configuration.account ?? authBox.authentication.user( let desiredAccount = configuration.account ?? meAcctDomain
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain
else { else {
return completion(.unconfigured) return completion(.unconfigured)
} }

View File

@ -86,9 +86,7 @@ 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( } else if let currentlyLoggedInAccount = try? await authBox.authentication.me().acctWithDomain {
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount] desiredAccounts = [currentlyLoggedInAccount]
} else { } else {
return completion(.unconfigured) return completion(.unconfigured)