From b00877d5da9eec680be19047e8a421c586261be2 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 17 Nov 2023 13:59:22 +0100 Subject: [PATCH] Continue refactoring of MastodonUser and Status (IOS-176, IOS-189) --- Mastodon/Diffable/Report/ReportSection.swift | 8 +- .../Provider/DataSourceFacade+Block.swift | 22 -- .../Provider/DataSourceFacade+Media.swift | 21 +- .../DataSourceFacade+SearchHistory.swift | 18 +- .../Scene/Account/AccountListViewModel.swift | 20 +- .../HomeTimelineViewController.swift | 35 ++- .../Share/AuthenticationViewModel.swift | 35 +-- .../Profile/About/ProfileAboutViewModel.swift | 11 +- .../FollowedTagsTableViewCell.swift | 6 +- ...owedTagsViewModel+DiffableDataSource.swift | 2 +- .../FollowedTags/FollowedTagsViewModel.swift | 23 +- .../Header/ProfileHeaderViewController.swift | 7 +- .../Header/ProfileHeaderViewModel.swift | 3 +- .../ProfileHeaderView+Configuration.swift | 50 ++--- .../Scene/Profile/ProfileViewController.swift | 34 ++- .../Root/MainTab/MainTabBarController.swift | 26 +-- .../Scene/Root/Sidebar/SidebarViewModel.swift | 4 +- .../SearchHistory/SearchHistorySection.swift | 12 +- .../SearchResult/SearchResultSection.swift | 8 +- .../SearchResultViewModel+State.swift | 8 +- .../MastodonStatusThreadViewModel.swift | 53 ++--- MastodonIntent/Model/Account+Fetch.swift | 32 ++- .../AuthenticationServiceProvider.swift | 9 +- .../MastodonCore/MastodonAuthentication.swift | 48 ++-- .../Service/API/APIService+Block.swift | 211 +++++++----------- .../Service/API/APIService+Follow.swift | 188 ++++++++-------- .../API/APIService+FollowRequest.swift | 25 +-- .../Service/API/APIService+Follower.swift | 24 +- .../Service/API/APIService+Following.swift | 25 --- .../Service/API/APIService+Mute.swift | 21 +- .../Service/API/APIService+Notification.swift | 16 +- .../Service/API/APIService+Tags.swift | 33 +-- .../Service/InstanceService.swift | 56 +---- .../Notification/NotificationService.swift | 49 ++-- .../Entity/Mastodon+Entity+Account.swift | 9 + .../Entity/Mastodon+Entity+Instance.swift | 18 ++ .../Entity/Mastodon+Entity+InstanceV2.swift | 10 + .../Entity/Mastodon+Entity+Poll.swift | 1 - .../Entity/Mastodon+Entity+Status.swift | 1 - .../ComposeContentViewModel.swift | 54 +++-- .../MastodonStatusEditPublisher.swift | 6 +- .../Publisher/MastodonStatusPublisher.swift | 6 +- .../Content/NotificationView+ViewModel.swift | 5 +- .../View/Content/StatusView+ViewModel.swift | 7 +- .../ViewModel/RelationshipViewModel.swift | 43 +++- .../FollowersCount/FollowersCountWidget.swift | 6 +- .../MultiFollowersCountWidget.swift | 4 +- 47 files changed, 538 insertions(+), 775 deletions(-) diff --git a/Mastodon/Diffable/Report/ReportSection.swift b/Mastodon/Diffable/Report/ReportSection.swift index 99e04ea1f..1d6162a9a 100644 --- a/Mastodon/Diffable/Report/ReportSection.swift +++ b/Mastodon/Diffable/Report/ReportSection.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import MastodonSDK import UIKit @@ -48,12 +46,11 @@ extension ReportSection { case .status(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } configure( context: context, tableView: tableView, cell: cell, - viewModel: .init(value: status), + viewModel: .init(value: record), configuration: configuration ) } @@ -78,8 +75,7 @@ extension ReportSection { case .result(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL())) + cell.avatarImageView.configure(configuration: .init(url: record.avatarImageURL())) } return cell case .bottomLoader: diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index c8f1f9405..02c06711a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -6,32 +6,10 @@ // import UIKit -import CoreDataStack import MastodonCore import MastodonSDK extension DataSourceFacade { - static func responseToUserBlockAction( - dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord - ) 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( dependency: NeedsDependency & AuthContextProvider, user: Mastodon.Entity.Account diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 21e67d8fd..7e5e6d35f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift @@ -6,7 +6,6 @@ // import UIKit -import CoreDataStack import MastodonUI import MastodonLocalization import MastodonSDK @@ -66,10 +65,8 @@ extension DataSourceFacade { previewContext: AttachmentPreviewContext ) async throws { let managedObjectContext = dependency.context.managedObjectContext - let attachments: [MastodonAttachment] = try await managedObjectContext.perform { - let status = status.reblog ?? status - return status.mastodonAttachments - } + let status = status.reblog ?? status + let attachments = status.mastodonAttachments let thumbnails = await previewContext.thumbnails() @@ -150,20 +147,14 @@ extension DataSourceFacade { @MainActor static func coordinateToMediaPreviewScene( dependency: NeedsDependency & MediaPreviewableViewController, - user: ManagedObjectRecord, + user: Mastodon.Entity.Account, previewContext: ImagePreviewContext ) async throws { let managedObjectContext = dependency.context.managedObjectContext - var _avatarAssetURL: String? - var _headerAssetURL: String? - - try await managedObjectContext.perform { - guard let user = user.object(in: managedObjectContext) else { return } - _avatarAssetURL = user.avatar - _headerAssetURL = user.header - } - + var _avatarAssetURL: String? = user.avatar + var _headerAssetURL: String? = user.header + let thumbnail = await previewContext.thumbnail() let source: MediaPreviewTransitionItem.Source = { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index ff97ffd19..0befe466f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreDataStack import MastodonCore import UIKit @@ -86,21 +85,10 @@ extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider ) async throws { let authenticationBox = provider.authContext.mastodonAuthenticationBox - let managedObjectContext = provider.context.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return } - let request = SearchHistory.sortedFetchRequest - 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 { … } + guard let _ = try? await authenticationBox.authentication.me() else { return } + + #warning("re-implement search history") } // end func } diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 5919596bf..beaeb2935 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -78,12 +78,13 @@ extension AccountListViewModel { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first { - AccountListViewModel.configure( - in: managedObjectContext, - cell: cell, - authentication: record, - activeAuthentication: activeAuthentication - ) + Task { @MainActor in + await AccountListViewModel.configure( + in: managedObjectContext, + cell: cell, + authentication: record, + activeAuthentication: activeAuthentication + )} } return cell case .addAccount: @@ -97,13 +98,14 @@ extension AccountListViewModel { diffableDataSource?.apply(snapshot) } + @MainActor static func configure( in context: NSManagedObjectContext, cell: AccountListTableViewCell, authentication: MastodonAuthentication, activeAuthentication: MastodonAuthentication - ) { - guard let user = authentication.user(in: context) else { return } + ) async { + guard let user = try? await authentication.me() else { return } // avatar cell.avatarButton.avatarImageView.configure( @@ -112,7 +114,7 @@ extension AccountListViewModel { // name 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) cell.nameLabel.configure(content: metaContent) } catch { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index ed363f397..a94021e83 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -8,8 +8,6 @@ import UIKit import AVKit import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import AlamofireImage @@ -206,24 +204,25 @@ extension HomeTimelineViewController { viewModel.timelineIsEmpty .receive(on: DispatchQueue.main) .sink { [weak self] isEmpty in - if isEmpty { - self?.showEmptyView() - - let userDoesntFollowPeople: Bool - if let managedObjectContext = self?.context.managedObjectContext, - let authContext = self?.authContext, - let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){ - userDoesntFollowPeople = me.followersCount == 0 + Task { @MainActor in + if isEmpty { + self?.showEmptyView() + + let userDoesntFollowPeople: Bool + if let authContext = self?.authContext, + let me = try? await authContext.mastodonAuthenticationBox.authentication.me() { + userDoesntFollowPeople = me.followersCount == 0 + } else { + userDoesntFollowPeople = true + } + + if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople { + self?.findPeopleButtonPressed(self) + self?.viewModel.presentedSuggestions = true + } } 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) diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 192eefb65..2d65b54fe 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -6,8 +6,6 @@ // import UIKit -import CoreData -import CoreDataStack import Combine import MastodonSDK import MastodonCore @@ -188,31 +186,36 @@ extension AuthenticationViewModel { ) -> AnyPublisher, Error> { let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken) let managedObjectContext = context.backgroundManagedObjectContext - +#warning("what happens if instancev2 is not reachable (errors out)??") return context.apiService.accountVerifyCredentials( domain: info.domain, authorization: authorization ) - .tryMap { response -> Mastodon.Response.Content in - let account = response.value - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { - throw AuthenticationError.badCredentials - } - + .flatMap { response in + Publishers.CombineLatest3( + Just(response).setFailureType(to: Error.self).eraseToAnyPublisher(), + Mastodon.API.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher(), + Mastodon.API.V2.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher() + ).eraseToAnyPublisher() + } + .tryMap { (accountResponse, instanceV1Response, instanceV2Response) -> Mastodon.Response.Content in + let account = accountResponse.value + let instanceV1 = instanceV1Response.value + let instanceV2 = instanceV2Response.value + AuthenticationServiceProvider.shared .authentications .insert(MastodonAuthentication.createFrom(domain: info.domain, - userID: mastodonUser.id, - username: mastodonUser.username, + userID: account.id, + username: account.username, appAccessToken: userToken.accessToken, // TODO: swap app token userAccessToken: userToken.accessToken, clientID: info.clientID, - clientSecret: info.clientSecret), at: 0) + clientSecret: info.clientSecret, + instance: instanceV1, + instanceV2: instanceV2), at: 0) - return response + return accountResponse } .eraseToAnyPublisher() } diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index f9a0b1c9d..55daafb92 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import MastodonSDK import MastodonMeta import MastodonCore @@ -19,7 +18,7 @@ final class ProfileAboutViewModel { // input let context: AppContext - @Published var user: MastodonUser? + @Published var user: Mastodon.Entity.Account? @Published var isEditing = false @Published var accountForEdit: Mastodon.Entity.Account? @@ -28,7 +27,7 @@ final class ProfileAboutViewModel { let profileInfo = ProfileInfo() let profileInfoEditing = ProfileInfo() - @Published var fields: [MastodonField] = [] + @Published var fields: [Mastodon.Entity.Field] = [] @Published var emojiMeta: MastodonContent.Emojis = [:] @Published var createdAt: Date = Date() @@ -38,18 +37,18 @@ final class ProfileAboutViewModel { $user .compactMap { $0 } - .flatMap { $0.publisher(for: \.emojis) } + .compactMap { $0.emojis } .map { $0.asDictionary } .assign(to: &$emojiMeta) $user .compactMap { $0 } - .flatMap { $0.publisher(for: \.fields) } + .compactMap { $0.fields } .assign(to: &$fields) $user .compactMap { $0 } - .flatMap { $0.publisher(for: \.createdAt) } + .compactMap { $0.createdAt } .assign(to: &$createdAt) Publishers.CombineLatest( diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift index 6adb15a9c..605fa2e86 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift @@ -6,13 +6,13 @@ // import UIKit -import CoreDataStack +import MastodonSDK final class FollowedTagsTableViewCell: UITableViewCell { private var hashtagView: HashtagTimelineHeaderView! private let separatorLine = UIView.separatorLine private weak var viewModel: FollowedTagsViewModel? - private weak var hashtag: Tag? + private var hashtag: Mastodon.Entity.Tag? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -67,7 +67,7 @@ private extension FollowedTagsTableViewCell { } extension FollowedTagsTableViewCell { - func populate(with tag: Tag) { + func populate(with tag: Mastodon.Entity.Tag) { self.hashtag = tag hashtagView.update(HashtagTimelineHeaderView.Data.from(tag)) } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift index 91f2cc5e9..e4391559f 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift @@ -18,7 +18,7 @@ extension FollowedTagsViewModel { } enum Item: Hashable { - case hashtag(Tag) + case hashtag(Mastodon.Entity.Tag) } func tableViewDiffableDataSource( diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift index f73094a8c..165654db6 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -8,14 +8,11 @@ import os import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore final class FollowedTagsViewModel: NSObject { var disposeBag = Set() - let fetchedResultsController: FollowedTagsFetchedResultController private weak var tableView: UITableView? var diffableDataSource: UITableViewDiffableDataSource? @@ -24,22 +21,18 @@ final class FollowedTagsViewModel: NSObject { let context: AppContext let authContext: AuthContext + @Published var records = [Mastodon.Entity.Tag]() + // output let presentHashtagTimeline = PassthroughSubject() init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FollowedTagsFetchedResultController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme: - ) super.init() - self.fetchedResultsController - .$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in 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 switch tag.following { - case true: + case .none: + break + case .some(true): _ = try? await context.apiService.unfollowTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox ) - case false: + case .some(false): _ = try? await context.apiService.followTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox @@ -94,7 +89,7 @@ extension FollowedTagsViewModel: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let object = fetchedResultsController.records[indexPath.row] + let object = records[indexPath.row] let hashtagTimelineViewModel = HashtagTimelineViewModel( context: self.context, diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index bc3c1dfa8..2cb664eaf 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import PhotosUI import AlamofireImage import CropViewController @@ -270,12 +269,11 @@ extension ProfileHeaderViewController { extension ProfileHeaderViewController: ProfileHeaderViewDelegate { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) { guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) Task { try await DataSourceFacade.coordinateToMediaPreviewScene( dependency: self, - user: record, + user: user, previewContext: DataSourceFacade.ImagePreviewContext( imageView: button.avatarImageView, containerView: .profileAvatar(profileHeaderView) @@ -286,12 +284,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) { guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) Task { try await DataSourceFacade.coordinateToMediaPreviewScene( dependency: self, - user: record, + user: user, previewContext: DataSourceFacade.ImagePreviewContext( imageView: imageView, containerView: .profileBanner(profileHeaderView) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 9301aea50..1120cc143 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import Kanna import MastodonSDK import MastodonMeta @@ -26,7 +25,7 @@ final class ProfileHeaderViewModel { let context: AppContext let authContext: AuthContext - @Published var user: MastodonUser? + @Published var user: Mastodon.Entity.Account? @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none @Published var isMyself = false diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift index 8e1693142..27544cef6 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift @@ -7,49 +7,33 @@ import UIKit import Combine -import CoreDataStack +import MastodonSDK extension ProfileHeaderView { - func configuration(user: MastodonUser) { + func configuration(user: Mastodon.Entity.Account) { // header - user.publisher(for: \.header) - .map { _ in user.headerImageURL() } - .assign(to: \.headerImageURL, on: viewModel) - .store(in: &disposeBag) + viewModel.headerImageURL = URL(string: user.header) + // avatar - user.publisher(for: \.avatar) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) + viewModel.avatarImageURL = user.avatarImageURL() + // emojiMeta - user.publisher(for: \.emojis) - .map { $0.asDictionary } - .assign(to: \.emojiMeta, on: viewModel) - .store(in: &disposeBag) + viewModel.emojiMeta = user.emojiMeta + // name - user.publisher(for: \.displayName) - .map { _ in user.displayNameWithFallback } - .assign(to: \.name, on: viewModel) - .store(in: &disposeBag) + viewModel.name = user.displayNameWithFallback + // username viewModel.acct = user.acctWithDomain // bio - user.publisher(for: \.note) - .assign(to: \.note, on: viewModel) - .store(in: &disposeBag) + viewModel.note = user.note + // dashboard - user.publisher(for: \.statusesCount) - .map { Int($0) } - .assign(to: \.statusesCount, on: viewModel) - .store(in: &disposeBag) - user.publisher(for: \.followingCount) - .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) + viewModel.statusesCount = user.statusesCount + + viewModel.followingCount = user.followingCount + + viewModel.followersCount = user.followersCount } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index a1c5e3925..de8335e03 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -13,9 +13,9 @@ import MastodonAsset import MastodonCore import MastodonUI import MastodonLocalization -import CoreDataStack import TabBarPager import XLPagerTabStrip +import MastodonSDK protocol ProfileViewModelEditable { var isEdited: Bool { get } @@ -237,7 +237,7 @@ extension ProfileViewController { items.append(self.favoriteBarButtonItem) items.append(self.bookmarkBarButtonItem) - if self.currentInstance?.canFollowTags == true { + if self.currentInstance?.version?.majorServerVersion(greaterThanOrEquals: 4) ?? false == true { items.append(self.followedTagsBarButtonItem) } @@ -400,7 +400,6 @@ extension ProfileViewController { return nil } let name = user.displayNameWithFallback - let _ = ManagedObjectRecord(objectID: user.objectID) var menuActions: [MastodonMenu.Action] = [ .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), @@ -408,9 +407,14 @@ extension ProfileViewController { .reportUser(.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) { - let showReblogs = me.showingReblogsBy.contains(user) + if let relationship, relationship.following { + let showReblogs = relationship.showingReblogs == true let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) menuActions.insert(.hideReblogs(context), at: 1) } @@ -525,11 +529,10 @@ extension ProfileViewController { @objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) { guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) Task { let _activityViewController = try await DataSourceFacade.createActivityViewController( dependency: self, - user: record + user: user ) guard let activityViewController = _activityViewController else { return } _ = self.coordinator.present( @@ -799,11 +802,10 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { break case .follow, .request, .pending, .following: guard let user = viewModel.user else { return } - let record = ManagedObjectRecord(objectID: user.objectID) Task { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: record + user: user ) } case .muting: @@ -815,13 +817,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), preferredStyle: .alert ) - let record = ManagedObjectRecord(objectID: user.objectID) let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in guard let self = self else { return } Task { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: record + user: user ) } } @@ -838,13 +839,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), preferredStyle: .alert ) - let record = ManagedObjectRecord(objectID: user.objectID) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in guard let self = self else { return } Task { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: record + user: user ) } } @@ -886,14 +886,12 @@ extension ProfileViewController: MastodonMenuDelegate { func menuAction(_ action: MastodonMenu.Action) { guard let user = viewModel.user else { return } - let userRecord: ManagedObjectRecord = .init(objectID: user.objectID) - Task { try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: DataSourceFacade.MenuContext( - author: userRecord, + author: user, statusViewModel: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem @@ -936,7 +934,7 @@ extension ProfileViewController: PagerTabStripNavigateable { } private extension ProfileViewController { - var currentInstance: Instance? { - authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext) + var currentInstance: Mastodon.Entity.V2.Instance? { + authContext.mastodonAuthenticationBox.authentication.instanceV2 } } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index cd0804b24..34abefb2a 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -7,12 +7,12 @@ import UIKit import Combine -import CoreDataStack import SafariServices import MastodonAsset import MastodonCore import MastodonLocalization import MastodonUI +import MastodonSDK class MainTabBarController: UITabBarController { @@ -131,7 +131,6 @@ class MainTabBarController: UITabBarController { private(set) var isReadyForWizardAvatarButton = false // output - var avatarURLObserver: AnyCancellable? @Published var avatarURL: URL? // haptic feedback @@ -262,14 +261,8 @@ extension MainTabBarController { .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } - if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) { - self.avatarURLObserver = user.publisher(for: \.avatar) - .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() - } + if let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount { + self.avatarURL = user.avatarImageURL() // a11y let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } @@ -281,8 +274,6 @@ extension MainTabBarController { self?.updateUserAccount() } .store(in: &self.disposeBag) - } else { - self.avatarURLObserver = nil } } .store(in: &disposeBag) @@ -457,16 +448,7 @@ extension MainTabBarController { authenticationBox: authContext.mastodonAuthenticationBox ) - if let user = authContext.mastodonAuthenticationBox.authentication.user( - in: context.managedObjectContext - ) { - user.update( - property: .init( - entity: profileResponse.value, - domain: authContext.mastodonAuthenticationBox.domain - ) - ) - } + authContext.mastodonAuthenticationBox.inMemoryCache.meAccount = profileResponse.value } } } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 441b8ca29..d74915fb0 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -75,7 +75,7 @@ extension SidebarViewModel { let imageURL: URL? = { switch item { case .me: - let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) + let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount return user?.avatarImageURL() default: return nil @@ -132,7 +132,7 @@ extension SidebarViewModel { } .store(in: &cell.disposeBag) 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 cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) default: diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift index b2dc5b8e4..e30034acf 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistorySection.swift @@ -6,9 +6,9 @@ // import UIKit -import CoreDataStack import MastodonCore import MastodonAsset +import MastodonSDK enum SearchHistorySection: Hashable { case main @@ -28,20 +28,18 @@ extension SearchHistorySection { configuration: Configuration ) -> UICollectionViewDiffableDataSource { - let userCellRegister = UICollectionView.CellRegistration> { cell, indexPath, item in + let userCellRegister = UICollectionView.CellRegistration { cell, indexPath, item in context.managedObjectContext.performAndWait { - guard let user = item.object(in: context.managedObjectContext) else { return } - cell.condensedUserView.configure(with: user) + cell.condensedUserView.configure(with: item) } } - let hashtagCellRegister = UICollectionView.CellRegistration> { cell, indexPath, item in + let hashtagCellRegister = UICollectionView.CellRegistration { cell, indexPath, item in context.managedObjectContext.performAndWait { - guard let hashtag = item.object(in: context.managedObjectContext) else { return } var contentConfiguration = cell.defaultContentConfiguration() contentConfiguration.image = UIImage(systemName: "magnifyingglass") contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color - contentConfiguration.text = "#" + hashtag.name + contentConfiguration.text = "#" + item.name cell.contentConfiguration = contentConfiguration } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 32a913587..778193cb1 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -44,13 +44,12 @@ extension SearchResultSection { case .user(let record): let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } configure( context: context, authContext: authContext, tableView: tableView, cell: cell, - viewModel: UserTableViewCell.ViewModel(user: user, + viewModel: UserTableViewCell.ViewModel(user: record, followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()), @@ -61,12 +60,11 @@ extension SearchResultSection { case .status(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } configure( context: context, tableView: tableView, cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .status(status)), + viewModel: StatusTableViewCell.ViewModel(value: .status(record)), configuration: configuration ) } @@ -126,7 +124,7 @@ extension SearchResultSection { configuration: Configuration ) { cell.configure( - me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), + me: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount, tableView: tableView, viewModel: viewModel, delegate: configuration.userTableViewCellDelegate diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index e332b13d9..b052a0c9b 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -136,13 +136,13 @@ extension SearchResultViewModel.State { // reset data source when the search is refresh if offset == nil { - viewModel.userFetchedResultsController.userIDs = [] - viewModel.statusFetchedResultsController.statusIDs = [] + viewModel.statusRecords = [] + viewModel.userRecords = [] viewModel.hashtags = [] } - viewModel.userFetchedResultsController.append(userIDs: userIDs) - viewModel.statusFetchedResultsController.append(statusIDs: statusIDs) + viewModel.userRecords.append(contentsOf: response.value.accounts) + viewModel.statusRecords.append(contentsOf: response.value.statuses) var hashtags = viewModel.hashtags for hashtag in response.value.hashtags where !hashtags.contains(hashtag) { diff --git a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift index ad69791b2..126eb3689 100644 --- a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift +++ b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift @@ -8,8 +8,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore import MastodonMeta @@ -20,7 +18,7 @@ final class MastodonStatusThreadViewModel { // input let context: AppContext - @Published private(set) var deletedObjectIDs: Set = Set() + @Published private(set) var deletedObjectIDs: Set = Set() // output @Published var __ancestors: [StatusItem] = [] @@ -41,7 +39,7 @@ final class MastodonStatusThreadViewModel { let newItems = items.filter { item in switch item { case .thread(let thread): - return !deletedObjectIDs.contains(thread.record.objectID) + return !deletedObjectIDs.contains(thread.record.id) default: assertionFailure() return false @@ -60,7 +58,7 @@ final class MastodonStatusThreadViewModel { let newItems = items.filter { item in switch item { case .thread(let thread): - return !deletedObjectIDs.contains(thread.record.objectID) + return !deletedObjectIDs.contains(thread.record.id) default: assertionFailure() return false @@ -81,14 +79,16 @@ extension MastodonStatusThreadViewModel { nodes: [Node] ) { let ids = nodes.map { $0.statusID } - var dictionary: [Status.ID: Status] = [:] + var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] do { - let request = Status.sortedFetchRequest - request.predicate = Status.predicate(domain: domain, ids: ids) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } +// let request = Status.sortedFetchRequest +// request.predicate = Status.predicate(domain: domain, ids: ids) +// let statuses = try self.context.managedObjectContext.fetch(request) + + #warning("figure out what this does") +// for status in statuses { +// dictionary[status.id] = status +// } } catch { return } @@ -98,9 +98,9 @@ extension MastodonStatusThreadViewModel { guard let status = dictionary[node.statusID] else { continue } let isLast = i == nodes.count - 1 - let record = ManagedObjectRecord(objectID: status.objectID) +// let record = ManagedObjectRecord(objectID: status.objectID) let context = StatusItem.Thread.Context( - status: record, + status: status, displayUpperConversationLink: !isLast, displayBottomConversationLink: true ) @@ -119,14 +119,15 @@ extension MastodonStatusThreadViewModel { let childrenIDs = nodes .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } .flatMap { $0 } - var dictionary: [Status.ID: Status] = [:] + var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] do { - let request = Status.sortedFetchRequest - request.predicate = Status.predicate(domain: domain, ids: childrenIDs) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } +// let request = Status.sortedFetchRequest +// request.predicate = Status.predicate(domain: domain, ids: childrenIDs) +// let statuses = try self.context.managedObjectContext.fetch(request) + #warning("what is this???") +// for status in statuses { +// dictionary[status.id] = status +// } } catch { return } @@ -135,9 +136,9 @@ extension MastodonStatusThreadViewModel { for node in nodes { guard let status = dictionary[node.statusID] else { continue } // first tier - let record = ManagedObjectRecord(objectID: status.objectID) +// let record = ManagedObjectRecord(objectID: status.objectID) let context = StatusItem.Thread.Context( - status: record + status: status ) let item = StatusItem.thread(.leaf(context: context)) newItems.append(item) @@ -145,9 +146,9 @@ extension MastodonStatusThreadViewModel { // second tier if let child = node.children.first { guard let secondaryStatus = dictionary[child.statusID] else { continue } - let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) +// let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) let secondaryContext = StatusItem.Thread.Context( - status: secondaryRecord, + status: secondaryStatus, displayUpperConversationLink: true ) let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) @@ -263,7 +264,7 @@ extension MastodonStatusThreadViewModel.Node { } extension MastodonStatusThreadViewModel { - func delete(objectIDs: [NSManagedObjectID]) { + func delete(objectIDs: [Mastodon.Entity.Status.ID]) { var set = deletedObjectIDs for objectID in objectIDs { set.insert(objectID) diff --git a/MastodonIntent/Model/Account+Fetch.swift b/MastodonIntent/Model/Account+Fetch.swift index f3d8ee344..adebf5504 100644 --- a/MastodonIntent/Model/Account+Fetch.swift +++ b/MastodonIntent/Model/Account+Fetch.swift @@ -16,24 +16,22 @@ extension Account { @MainActor static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] { // get accounts - let accounts: [Account] = try await managedObjectContext.perform { - let results = AuthenticationServiceProvider.shared.authentications - let accounts = results.compactMap { mastodonAuthentication -> Account? in - guard let user = mastodonAuthentication.user(in: managedObjectContext) else { - return nil - } - let 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 + let results = AuthenticationServiceProvider.shared.authentications + var accounts = [Account]() + for mastodonAuthentication in results { + guard let user = try? await mastodonAuthentication.me() else { + continue } - return accounts - } // end managedObjectContext.perform + 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 + accounts.append(account) + } return accounts } diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 1d8b2d6cc..eee315c3c 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -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 guard authentication.domain == domain else { return authentication } return authentication.updating(instance: instance) } } + func update(instanceV2: Mastodon.Entity.V2.Instance, where domain: String) { + authentications = authentications.map { authentication in + guard authentication.domain == domain else { return authentication } + return authentication.updating(instanceV2: instanceV2) + } + } + func delete(authentication: MastodonAuthentication) { authentications.removeAll(where: { $0 == authentication }) } diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 0b1a9db49..90f3bff64 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -1,7 +1,6 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation -import CoreDataStack import MastodonSDK public struct MastodonAuthentication: Codable, Hashable { @@ -21,7 +20,9 @@ public struct MastodonAuthentication: Codable, Hashable { public private(set) var activedAt: Date public private(set) var userID: String - public private(set) var instanceObjectIdURI: URL? + + public private(set) var instance: Mastodon.Entity.Instance? + public private(set) var instanceV2: Mastodon.Entity.V2.Instance? internal var persistenceIdentifier: String { "\(username)@\(domain)" @@ -34,7 +35,9 @@ public struct MastodonAuthentication: Codable, Hashable { appAccessToken: String, userAccessToken: String, clientID: String, - clientSecret: String + clientSecret: String, + instance: Mastodon.Entity.Instance?, + instanceV2: Mastodon.Entity.V2.Instance? ) -> Self { let now = Date() return MastodonAuthentication( @@ -49,7 +52,8 @@ public struct MastodonAuthentication: Codable, Hashable { updatedAt: now, activedAt: now, userID: userID, - instanceObjectIdURI: nil + instance: instance, + instanceV2: instanceV2 ) } @@ -65,7 +69,8 @@ public struct MastodonAuthentication: Codable, Hashable { updatedAt: Date? = nil, activedAt: Date? = nil, userID: String? = nil, - instanceObjectIdURI: URL? = nil + instance: Mastodon.Entity.Instance? = nil, + instanceV2: Mastodon.Entity.V2.Instance? = nil ) -> Self { MastodonAuthentication( identifier: identifier ?? self.identifier, @@ -79,31 +84,28 @@ public struct MastodonAuthentication: Codable, Hashable { updatedAt: updatedAt ?? self.updatedAt, activedAt: activedAt ?? self.activedAt, 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 - return instance + func updating(instance: Mastodon.Entity.Instance) -> Self { + copy(instance: instance) } - - public func user(in context: NSManagedObjectContext) -> MastodonUser? { - let userPredicate = MastodonUser.predicate(domain: domain, id: userID) - return MastodonUser.findOrFetch(in: context, matching: userPredicate) - } - - func updating(instance: Instance) -> Self { - copy(instanceObjectIdURI: instance.objectID.uriRepresentation()) + + func updating(instanceV2: Mastodon.Entity.V2.Instance) -> Self { + copy(instanceV2: instanceV2) } func updating(activatedAt: Date) -> Self { 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 + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index c1650e9b5..14a9b7af5 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -7,15 +7,13 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { private struct MastodonBlockContext { - let sourceUserID: MastodonUser.ID - let targetUserID: MastodonUser.ID + let sourceUserID: Mastodon.Entity.Account.ID + let targetUserID: Mastodon.Entity.Account.ID let targetUsername: String let isBlocking: Bool let isFollowing: Bool @@ -41,113 +39,92 @@ extension APIService { limit: limit, authorization: authenticationBox.userAuthorization ).singleOutput() - - let userIDs = response.value.map { $0.id } - let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs) - - let fetchRequest = MastodonUser.fetchRequest() - fetchRequest.predicate = predicate - fetchRequest.includesPropertyValues = false - - try await managedObjectContext.performChanges { - let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser] - - for user in users { - user.deleteStatusAndNotificationFeeds(in: managedObjectContext) - } - } - + return response } - public func toggleBlock( - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext - let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } - - let isBlocking = user.blockingBy.contains(me) - let isFollowing = user.followingBy.contains(me) - // toggle block state - user.update(isBlocking: !isBlocking, by: me) - // update follow state implicitly - if !isBlocking { - // will do block action. set to unfollow - user.update(isFollowing: false, by: me) - } - - return MastodonBlockContext( - sourceUserID: me.id, - targetUserID: user.id, - targetUsername: user.username, - isBlocking: isBlocking, - isFollowing: isFollowing - ) - } - - let result: Result, Error> - do { - if blockContext.isBlocking { - let response = try await Mastodon.API.Account.unblock( - session: session, - domain: authenticationBox.domain, - accountID: blockContext.targetUserID, - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } else { - let response = try await Mastodon.API.Account.block( - session: session, - domain: authenticationBox.domain, - accountID: blockContext.targetUserID, - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } - } catch { - result = .failure(error) - } - - try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { return } - - - switch result { - case .success(let response): - let relationship = response.value - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: relationship, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isBlocking: blockContext.isBlocking, by: me) - user.update(isFollowing: blockContext.isFollowing, by: me) - } - } - - let response = try result.get() - return response - } +// public func toggleBlock( +// user: Mastodon.Entity.Account, +// authenticationBox: MastodonAuthenticationBox +// ) async throws -> Mastodon.Response.Content { +// +//// let managedObjectContext = backgroundManagedObjectContext +//// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { +//// let authentication = authenticationBox.authentication +//// +// guard +//// let user = user.object(in: managedObjectContext), +// let me = authenticationBox.inMemoryCache.meAccount, +// let relationship = try await Mastodon.API.Account.relationships( +// session: session, +// domain: authenticationBox.domain, +// query: .init(ids: [user.id]), +// authorization: authenticationBox.userAuthorization +// ).singleOutput().value.first +// else { +// throw APIError.implicit(.badRequest) +// } +//// +//// let isBlocking = user.blockingBy.contains(me) +//// let isFollowing = user.followingBy.contains(me) +//// // toggle block state +//// user.update(isBlocking: !isBlocking, by: me) +//// // update follow state implicitly +//// if !isBlocking { +//// // will do block action. set to unfollow +//// user.update(isFollowing: false, by: me) +//// } +//// +//// return MastodonBlockContext( +//// sourceUserID: me.id, +//// targetUserID: user.id, +//// targetUsername: user.username, +//// isBlocking: isBlocking, +//// isFollowing: isFollowing +//// ) +//// } +// +// +// +// let blockContext = MastodonBlockContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// targetUsername: user.username, +// isBlocking: !relationship.blocking, +// isFollowing: { +// if !relationship.blocking { +// return false +// } +// return relationship.following +// }() +// ) +// +// let result: Result, Error> +// do { +// if blockContext.isBlocking { +// let response = try await Mastodon.API.Account.unblock( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } else { +// let response = try await Mastodon.API.Account.block( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } +// } catch { +// result = .failure(error) +// } +// +// let response = try result.get() +// return response +// } public func toggleBlock( user: Mastodon.Entity.Account, @@ -178,21 +155,3 @@ extension APIService { 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) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 301417a99..c6355ac50 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -7,111 +7,109 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { private struct MastodonFollowContext { - let sourceUserID: MastodonUser.ID - let targetUserID: MastodonUser.ID + let sourceUserID: Mastodon.Entity.Account.ID + let targetUserID: Mastodon.Entity.Account.ID let isFollowing: Bool let isPending: Bool let needsUnfollow: Bool } - /// Toggle friendship between target MastodonUser and current MastodonUser - /// - /// Following / Following pending <-> Unfollow - /// - /// - Parameters: - /// - mastodonUser: target MastodonUser - /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` - /// - Returns: publisher for `Relationship` - public func toggleFollow( - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext - let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } - guard let user = user.object(in: managedObjectContext) else { return nil } - - let isFollowing = user.followingBy.contains(me) - let isPending = user.followRequestedBy.contains(me) - let needsUnfollow = isFollowing || isPending - - if needsUnfollow { - // unfollow - user.update(isFollowing: false, by: me) - user.update(isFollowRequested: false, by: me) - } else { - // follow - if user.locked { - user.update(isFollowing: false, by: me) - user.update(isFollowRequested: true, by: me) - } else { - user.update(isFollowing: true, by: me) - user.update(isFollowRequested: false, by: me) - } - } - let context = MastodonFollowContext( - sourceUserID: me.id, - targetUserID: user.id, - isFollowing: isFollowing, - isPending: isPending, - needsUnfollow: needsUnfollow - ) - return context - } - - guard let followContext = _followContext else { - throw APIError.implicit(.badRequest) - } - - // request follow or unfollow - let result: Result, Error> - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: followContext.targetUserID, - followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } - - // update friendship state - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext), - let user = user.object(in: managedObjectContext) - else { return } - - switch result { - case .success(let response): - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isFollowing: followContext.isFollowing, by: me) - user.update(isFollowRequested: followContext.isPending, by: me) - } - } - - let response = try result.get() - return response - } +// /// Toggle friendship between target MastodonUser and current MastodonUser +// /// +// /// Following / Following pending <-> Unfollow +// /// +// /// - Parameters: +// /// - mastodonUser: target MastodonUser +// /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` +// /// - Returns: publisher for `Relationship` +// public func toggleFollow( +// user: ManagedObjectRecord, +// authenticationBox: MastodonAuthenticationBox +// ) async throws -> Mastodon.Response.Content { +// +// let managedObjectContext = backgroundManagedObjectContext +// let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { +// guard let me = authenticationBox.inMemoryCache.meAccount else { return nil } +// guard let user = user.object(in: managedObjectContext) else { return nil } +// +// let isFollowing = user.followingBy.contains(me) +// let isPending = user.followRequestedBy.contains(me) +// let needsUnfollow = isFollowing || isPending +// +// if needsUnfollow { +// // unfollow +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: false, by: me) +// } else { +// // follow +// if user.locked { +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: true, by: me) +// } else { +// user.update(isFollowing: true, by: me) +// user.update(isFollowRequested: false, by: me) +// } +// } +// let context = MastodonFollowContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// isFollowing: isFollowing, +// isPending: isPending, +// needsUnfollow: needsUnfollow +// ) +// return context +// } +// +// guard let followContext = _followContext else { +// throw APIError.implicit(.badRequest) +// } +// +// // request follow or unfollow +// let result: Result, Error> +// do { +// let response = try await Mastodon.API.Account.follow( +// session: session, +// domain: authenticationBox.domain, +// accountID: followContext.targetUserID, +// followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } catch { +// result = .failure(error) +// } +// +// // update friendship state +// try await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext), +// let user = user.object(in: managedObjectContext) +// else { return } +// +// switch result { +// case .success(let response): +// Persistence.MastodonUser.update( +// mastodonUser: user, +// context: Persistence.MastodonUser.RelationshipContext( +// entity: response.value, +// me: me, +// networkDate: response.networkDate +// ) +// ) +// case .failure: +// // rollback +// user.update(isFollowing: followContext.isFollowing, by: me) +// user.update(isFollowRequested: followContext.isPending, by: me) +// } +// } +// +// let response = try result.get() +// return response +// } public func toggleFollow( user: Mastodon.Entity.Account, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index f90aab5d3..c5f902901 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { @@ -25,28 +23,7 @@ extension APIService { query: query, authorization: authenticationBox.userAuthorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate( - domain: authenticationBox.domain, - id: authenticationBox.userID - ) - request.fetchLimit = 1 - guard let user = managedObjectContext.safeFetch(request).first else { return } - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift index f463501f6..d425adb9b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { @@ -32,27 +30,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - for entity in response.value { - let result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - - let user = result.user - me?.update(isFollowing: true, by: user) - } - } - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index 683a98166..465b1880c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { @@ -33,30 +31,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - for entity in response.value { - let result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - - if let me = me { - let user = result.user - user.update(isFollowing: true, by: me) - } - } - - } - return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index b676a0914..623ff45d8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -7,15 +7,13 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { private struct MastodonMuteContext { - let sourceUserID: MastodonUser.ID - let targetUserID: MastodonUser.ID + let sourceUserID: Mastodon.Entity.Account.ID + let targetUserID: Mastodon.Entity.Account.ID let targetUsername: String let isMuting: Bool } @@ -41,21 +39,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let userIDs = response.value.map { $0.id } - let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs) - - let fetchRequest = MastodonUser.fetchRequest() - fetchRequest.predicate = predicate - fetchRequest.includesPropertyValues = false - - try await managedObjectContext.performChanges { - let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser] - - for user in users { - user.deleteStatusAndNotificationFeeds(in: managedObjectContext) - } - } - return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 3fd871bba..2c02ad17e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -170,21 +170,7 @@ extension APIService { notificationID: notificationID, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - _ = Persistence.Notification.createOrMerge( - in: managedObjectContext, - context: Persistence.Notification.PersistContext( - domain: domain, - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift index 0a61fc687..819bf5afe 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { @@ -27,7 +25,7 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) + return response } // end func public func followTag( @@ -44,7 +42,7 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) + return response } // end func public func unfollowTag( @@ -61,31 +59,6 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) + return response } // end func } - -fileprivate extension APIService { - func persistTag( - from response: Mastodon.Response.Content, - domain: String, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - 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 - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 0d1509af8..61cf4a236 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import MastodonSDK public final class InstanceService { @@ -16,7 +14,6 @@ public final class InstanceService { var disposeBag = Set() // input - let backgroundManagedObjectContext: NSManagedObjectContext weak var apiService: APIService? weak var authenticationService: AuthenticationService? @@ -26,7 +23,6 @@ public final class InstanceService { apiService: APIService, authenticationService: AuthenticationService ) { - self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext self.apiService = apiService self.authenticationService = authenticationService @@ -68,57 +64,13 @@ extension InstanceService { } private func updateInstance(domain: String, response: Mastodon.Response.Content) -> AnyPublisher { - let managedObjectContext = self.backgroundManagedObjectContext - return managedObjectContext.performChanges { - // 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() + AuthenticationServiceProvider.shared.update(instance: response.value, where: domain) + return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher() } private func updateInstanceV2(domain: String, response: Mastodon.Response.Content) -> AnyPublisher { - let managedObjectContext = self.backgroundManagedObjectContext - return managedObjectContext.performChanges { - // get instance - let (instance, _) = APIService.CoreData.createOrMergeInstance( - in: managedObjectContext, - context: .init( - domain: domain, - entity: response.value, - networkDate: response.networkDate - ) - ) - - // 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() + AuthenticationServiceProvider.shared.update(instanceV2: response.value, where: domain) + return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher() } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index f7c6c08cf..9f11ce68a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCommon import MastodonLocalization @@ -96,32 +94,29 @@ extension NotificationService { extension NotificationService { public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] { - guard let authenticationService = self.authenticationService else { return [] } - let managedObjectContext = authenticationService.managedObjectContext - return try await managedObjectContext.perform { - var items: [UIApplicationShortcutItem] = [] - for authentication in AuthenticationServiceProvider.shared.authentications { - guard let user = authentication.user(in: managedObjectContext) else { continue } - let accessToken = authentication.userAccessToken - 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 item = UIApplicationShortcutItem( - type: NotificationService.unreadShortcutItemIdentifier, - localizedTitle: title, - localizedSubtitle: subtitle, - icon: nil, - userInfo: [ - "accessToken": accessToken as NSSecureCoding - ] - ) - items.append(item) - } - return items +// guard let authenticationService = self.authenticationService else { return [] } + var items: [UIApplicationShortcutItem] = [] + for authentication in AuthenticationServiceProvider.shared.authentications { + guard let user = try? await authentication.me() else { continue } + let accessToken = authentication.userAccessToken + 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 item = UIApplicationShortcutItem( + type: NotificationService.unreadShortcutItemIdentifier, + localizedTitle: title, + localizedSubtitle: subtitle, + icon: nil, + userInfo: [ + "accessToken": accessToken as NSSecureCoding + ] + ) + items.append(item) } + return items } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 5a308d35b..2ddc3d91c 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -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 { guard acct.contains("@") else { return "\(acct)@\(localDomain)" diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index 353601cd4..1cfb822d3 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -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) {} +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift index 3cfb29699..1bd2976a2 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift @@ -110,3 +110,13 @@ extension Mastodon.Entity.V2.Instance { 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) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift index e3405773b..72f224b84 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift @@ -45,7 +45,6 @@ extension Mastodon.Entity { case voted case ownVotes = "own_votes" case options - case isVoting } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index a3d8b9018..0569ae7ec 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -73,7 +73,6 @@ extension Mastodon.Entity { case visibility case sensitive - case sensitiveToggled case spoilerText = "spoiler_text" case mediaAttachments = "media_attachments" diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 117a4edb5..9fe241238 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -153,21 +153,17 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.authContext = authContext self.destination = destination self.composeContext = composeContext + self.visibility = { // default private when user locked 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 author.locked ? .private : .public }() // set visibility for reply post if case .reply(let status) = destination { -// context.managedObjectContext.performAndWait { -// guard let status = record.object(in: context.managedObjectContext) else { -// assertionFailure() -// return -// } let repliedStatusVisibility = status.visibility switch repliedStatusVisibility { case .public, .unlisted: @@ -225,7 +221,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // assertionFailure() // return // } - let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + let author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount var mentionAccts: [String] = [] if author?.id != status.account.id { @@ -258,11 +254,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // set limit let _configuration: Mastodon.Entity.Instance.Configuration? = { - var configuration: Mastodon.Entity.Instance.Configuration? = nil - context.managedObjectContext.performAndWait { - let authentication = authContext.mastodonAuthenticationBox.authentication - configuration = authentication.instance(in: context.managedObjectContext)?.configuration - } + let authentication = authContext.mastodonAuthenticationBox.authentication + var configuration: Mastodon.Entity.Instance.Configuration? = authentication.instance?.configuration return configuration }() if let configuration = _configuration { @@ -319,7 +312,7 @@ extension ComposeContentViewModel { $authContext .sink { [weak self] authContext in guard let self = self else { return } - guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } + guard let user = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else { return } self.avatarURL = user.avatarImageURL() self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) self.username = user.acctWithDomain @@ -563,10 +556,7 @@ extension ComposeContentViewModel { // author let managedObjectContext = self.context.managedObjectContext - var _author: ManagedObjectRecord? - managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord - } + var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount guard let author = _author else { throw AppError.badAuthentication } @@ -619,10 +609,7 @@ extension ComposeContentViewModel { // author let managedObjectContext = self.context.managedObjectContext - var _author: ManagedObjectRecord? - managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord - } + var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount guard let author = _author else { 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 + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift index 40f2ce781..d6436ef49 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift @@ -1,8 +1,6 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation -import CoreData -import CoreDataStack import MastodonCore import MastodonSDK import Combine @@ -11,7 +9,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { // Input public let statusID: Mastodon.Entity.Status.ID - public let author: ManagedObjectRecord + public let author: Mastodon.Entity.Account // content warning public let isContentWarningComposing: Bool @@ -41,7 +39,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { public init( statusID: Mastodon.Entity.Status.ID, - author: ManagedObjectRecord, + author: Mastodon.Entity.Account, isContentWarningComposing: Bool, contentWarning: String, content: String, diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 8f9e07914..b552fb0a5 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import MastodonCore import MastodonSDK @@ -17,7 +15,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { // Input // author - public let author: ManagedObjectRecord + public let author: Mastodon.Entity.Account? // refer public let replyTo: Mastodon.Entity.Status? // content warning @@ -47,7 +45,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { public var reactor: StatusPublisherReactor? public init( - author: ManagedObjectRecord, + author: Mastodon.Entity.Account, replyTo: Mastodon.Entity.Status?, isContentWarningComposing: Bool, contentWarning: String, diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 3a0f52fab..e6d8ad7c1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -220,7 +220,7 @@ extension NotificationView.ViewModel { ) ) .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 return } @@ -228,8 +228,7 @@ extension NotificationView.ViewModel { let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed let authentication = authContext.mastodonAuthenticationBox.authentication - let instance = authentication.instance(in: context.managedObjectContext) - let isTranslationEnabled = instance?.isTranslationEnabled ?? false + let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false let menuContext = NotificationView.AuthorMenuContext( name: name, diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index cec7b4a2b..928f42155 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import Meta import MastodonAsset import MastodonCore @@ -668,14 +666,13 @@ extension StatusView.ViewModel { let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo 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 return } let authentication = authContext.mastodonAuthenticationBox.authentication - let instance = authentication.instance(in: context.managedObjectContext) - let isTranslationEnabled = instance?.isTranslationEnabled ?? false + let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false let menuContext = StatusAuthorView.AuthorMenuContext( name: name, diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index a88dbbc05..bf773eadf 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -10,6 +10,11 @@ import Combine import MastodonAsset import MastodonLocalization import MastodonSDK +import MastodonCore + +enum RelationshipError: Error { + case FailedToResolveUser +} public enum RelationshipAction: Int, CaseIterable { case showReblogs @@ -127,8 +132,39 @@ public final class RelationshipViewModel { relationshipUpdatePublisher ) .receive(on: DispatchQueue.main) - .sink { [weak self] user, me, _ in - guard let self = self else { return } + .compactMap { user, me, _ -> Optional<(Mastodon.Entity.Account, Mastodon.Entity.Account, MastodonAuthentication)> in + 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) // guard let user = user, let me = me else { @@ -149,8 +185,7 @@ public final class RelationshipViewModel { // guard let self = self else { return } // self.relationshipUpdatePublisher.send() // } - } - .store(in: &disposeBag) +// } } } diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 1f118aeeb..e05dba26d 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -82,10 +82,10 @@ private extension FollowersCountWidgetProvider { return completion(.unconfigured) } + let meAcctDomain = try? await authBox.authentication.me().acctWithDomain + guard - let desiredAccount = configuration.account ?? authBox.authentication.user( - in: WidgetExtension.appContext.managedObjectContext - )?.acctWithDomain + let desiredAccount = configuration.account ?? meAcctDomain else { return completion(.unconfigured) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 15c1c4d14..b3a908e4f 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -86,9 +86,7 @@ private extension MultiFollowersCountWidgetProvider { if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) { desiredAccounts = configuredAccounts - } else if let currentlyLoggedInAccount = authBox.authentication.user( - in: WidgetExtension.appContext.managedObjectContext - )?.acctWithDomain { + } else if let currentlyLoggedInAccount = try? await authBox.authentication.me().acctWithDomain { desiredAccounts = [currentlyLoggedInAccount] } else { return completion(.unconfigured)