From 2be8d5b8dfb3d9fd72b4ac228e23234e854693c9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 7 Dec 2023 13:35:33 +0100 Subject: [PATCH] WIP: Comment out and replace user with status (IOS-192) --- .../Provider/DataSourceFacade+Follow.swift | 9 +- .../Provider/DataSourceFacade+Mute.swift | 8 +- .../Provider/DataSourceFacade+Profile.swift | 147 +++--- .../Provider/DataSourceFacade+Status.swift | 56 +-- ...er+NotificationTableViewCellDelegate.swift | 52 +-- ...Provider+StatusTableViewCellDelegate.swift | 77 ++-- ...taSourceProvider+UITableViewDelegate.swift | 54 +-- .../Provider/DataSourceProvider.swift | 1 + .../NotificationTimelineViewController.swift | 30 +- .../Profile/About/ProfileAboutViewModel.swift | 35 +- .../Header/ProfileHeaderViewController.swift | 128 +++--- .../Header/ProfileHeaderViewModel.swift | 4 +- .../ProfileHeaderView+Configuration.swift | 81 ++-- .../View/ProfileHeaderView+ViewModel.swift | 108 ++--- .../Header/View/ProfileHeaderView.swift | 24 - .../Scene/Profile/MeProfileViewModel.swift | 25 +- .../Scene/Profile/ProfileViewController.swift | 426 ++++++++---------- Mastodon/Scene/Profile/ProfileViewModel.swift | 160 +++---- .../Profile/RemoteProfileViewModel.swift | 59 +-- .../Report/Report/ReportViewController.swift | 4 +- .../Scene/Report/Report/ReportViewModel.swift | 109 ++--- .../ReportResultViewController.swift | 6 +- .../ReportResult/ReportResultViewModel.swift | 16 +- .../ReportStatusViewModel+State.swift | 12 +- .../ReportStatus/ReportStatusViewModel.swift | 6 +- .../ReportSupplementaryViewModel.swift | 6 +- ...ultViewController+DataSourceProvider.swift | 5 +- .../Scene/Settings/SettingsCoordinator.swift | 11 +- .../MastodonCore/MastodonAuthentication.swift | 8 +- .../Service/API/APIService+Account.swift | 32 +- .../Service/API/APIService+Follow.swift | 76 +--- .../Service/API/APIService+Mute.swift | 69 +-- 32 files changed, 732 insertions(+), 1112 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index e3445115d..c0b4cca77 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -120,11 +120,12 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToShowHideReblogAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) async throws { - _ = try await dependency.context.apiService.toggleShowReblogs( - for: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox) + #warning("TODO: Implement") +// _ = try await dependency.context.apiService.toggleShowReblogs( +// for: user, +// authenticationBox: dependency.authContext.mastodonAuthenticationBox) } static func responseToShowHideReblogAction( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 1db94bd4f..4e37bc7a5 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -6,20 +6,20 @@ // import UIKit -import CoreDataStack +import MastodonSDK import MastodonCore extension DataSourceFacade { static func responseToUserMuteAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleMute( - user: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox, + account: account ) } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 82ec945fb..464ecd374 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -45,21 +45,67 @@ extension DataSourceFacade { account: redirectRecord ) } - + @MainActor static func coordinateToProfileScene( provider: ViewControllerWithDependencies & AuthContextProvider, - user: ManagedObjectRecord + username: String, + domain: String ) async { - guard let user = user.object(in: provider.context.managedObjectContext) else { - assertionFailure() - return + provider.coordinator.showLoading() + + Task { + do { + guard let account = try await provider.context.apiService.fetchUser(username: username, + domain: domain, + authenticationBox: provider.authContext.mastodonAuthenticationBox) else { + return provider.coordinator.hideLoading() + } + + provider.coordinator.hideLoading() + + await coordinateToProfileScene(provider: provider, account: account) + } catch { + provider.coordinator.hideLoading() + } } - + } + + @MainActor + static func coordinateToProfileScene( + provider: ViewControllerWithDependencies & AuthContextProvider, + domain: String, + accountID: String + ) async { + provider.coordinator.showLoading() + + Task { + do { + let account = try await provider.context.apiService.accountInfo( + domain: domain, + userID: accountID, + authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization + ).value + + provider.coordinator.hideLoading() + + await coordinateToProfileScene(provider: provider, account: account) + } catch { + provider.coordinator.hideLoading() + } + } + } + + @MainActor + static func coordinateToProfileScene( + provider: ViewControllerWithDependencies & AuthContextProvider, + account: Mastodon.Entity.Account + ) { + let profileViewModel = ProfileViewModel( context: provider.context, authContext: provider.authContext, - optionalMastodonUser: user + account: account ) _ = provider.coordinator.present( @@ -68,31 +114,6 @@ extension DataSourceFacade { transition: .show ) } - - @MainActor - static func coordinateToProfileScene( - provider: ViewControllerWithDependencies & AuthContextProvider, - account: Mastodon.Entity.Account - ) async { - provider.coordinator.showLoading() - - guard let domain = account.domain else { return provider.coordinator.hideLoading() } - - Task { - do { - let user = try await provider.context.apiService.fetchUser(username: account.username, - domain: domain, - authenticationBox: provider.authContext.mastodonAuthenticationBox) - provider.coordinator.hideLoading() - - if let user { - await coordinateToProfileScene(provider: provider, user: user.asRecord) - } - } catch { - provider.coordinator.hideLoading() - } - } - } } extension DataSourceFacade { @@ -112,42 +133,31 @@ extension DataSourceFacade { else { return } - let mentions = status.entity.mentions ?? [] - + guard let mention = mentions.first(where: { $0.url == href }) else { - _ = provider.coordinator.present( + _ = provider.coordinator.present( scene: .safari(url: url), from: provider, transition: .safariPresent(animated: true, completion: nil) ) + return } - let userID = mention.id - let profileViewModel: ProfileViewModel = { - // check if self - guard userID != provider.authContext.mastodonAuthenticationBox.userID else { - return MeProfileViewModel(context: provider.context, authContext: provider.authContext) - } - - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: userID) - let _user = provider.context.managedObjectContext.safeFetch(request).first - - if let user = _user { - return ProfileViewModel(context: provider.context, authContext: provider.authContext, optionalMastodonUser: user) - } else { - return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID) - } - }() - - _ = provider.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: provider, - transition: .show - ) +#warning("TODO: Implement") + await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: "", accountID: mention.id) +// let profileViewModel = ProfileViewModel( +// context: provider.context, +// authContext: provider.authContext, +// account: status.entity.account +// ) +// +// _ = provider.coordinator.present( +// scene: .profile(viewModel: profileViewModel), +// from: provider, +// transition: .show +// ) } } @@ -166,20 +176,11 @@ extension DataSourceFacade { static func createActivityViewController( dependency: NeedsDependency, - user: ManagedObjectRecord - ) async throws -> UIActivityViewController? { - let managedObjectContext = dependency.context.managedObjectContext - let activityItems: [Any] = try await managedObjectContext.perform { - guard let user = user.object(in: managedObjectContext) else { return [] } - return user.activityItems - } - guard !activityItems.isEmpty else { - assertionFailure() - return nil - } - - let activityViewController = await UIActivityViewController( - activityItems: activityItems, + account: Mastodon.Entity.Account + ) -> UIActivityViewController { + + let activityViewController = UIActivityViewController( + activityItems: [account.url], applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)] ) return activityViewController diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 26e11d026..375c48575 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -144,8 +144,7 @@ extension DataSourceFacade { extension DataSourceFacade { struct MenuContext { - let author: ManagedObjectRecord? // todo: Remove once IOS-192 is ready - let authorEntity: Mastodon.Entity.Account? + let author: Mastodon.Entity.Account let statusViewModel: StatusView.ViewModel? let button: UIButton? let barButtonItem: UIBarButtonItem? @@ -176,17 +175,9 @@ extension DataSourceFacade { guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - - guard let user = _user else { return } - try await DataSourceFacade.responseToShowHideReblogAction( dependency: dependency, - user: user + account: menuContext.author ) } } @@ -207,17 +198,11 @@ extension DataSourceFacade { title: actionContext.isMuting ? L10n.Common.Controls.Friendship.unmute : L10n.Common.Controls.Friendship.mute, style: .destructive ) { [weak dependency] _ in - guard let dependency = dependency else { return } + guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - guard let user = _user else { return } try await DataSourceFacade.responseToUserMuteAction( dependency: dependency, - user: user + account: menuContext.author ) } // end Task } @@ -235,19 +220,13 @@ extension DataSourceFacade { title: actionContext.isBlocking ? L10n.Common.Controls.Friendship.unblock : L10n.Common.Controls.Friendship.block, style: .destructive ) { [weak dependency] _ in - guard let dependency = dependency else { return } + guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - guard let user = _user else { return } try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, - user: user + user: menuContext.author ) - } // end Task + } } alertController.addAction(confirmAction) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) @@ -255,12 +234,11 @@ extension DataSourceFacade { dependency.present(alertController, animated: true) case .reportUser: Task { - guard let user = menuContext.author else { return } - + let reportViewModel = ReportViewModel( context: dependency.context, authContext: dependency.authContext, - user: user, + account: menuContext.author, status: menuContext.statusViewModel?.originalStatus ) @@ -272,15 +250,11 @@ extension DataSourceFacade { } // end Task case .shareUser: - guard let user = menuContext.author else { - assertionFailure() - return - } - let _activityViewController = try await DataSourceFacade.createActivityViewController( + let activityViewController = DataSourceFacade.createActivityViewController( dependency: dependency, - user: user + account: menuContext.author ) - guard let activityViewController = _activityViewController else { return } + _ = dependency.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, @@ -303,7 +277,6 @@ extension DataSourceFacade { } // end Task case .shareStatus: Task { - let managedObjectContext = dependency.context.managedObjectContext guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else { assertionFailure() return @@ -380,11 +353,8 @@ extension DataSourceFacade { // do nothing, as the translation is reverted in `StatusTableViewCellDelegate` in `DataSourceProvider+StatusTableViewCellDelegate.swift`. break case .followUser(_): - - guard let author = menuContext.author else { return } - try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, - user: author) + user: menuContext.author) } } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 5ed4fc8b1..e1d01a357 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -31,20 +31,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - return .init(objectID: notification.account.objectID) - } - guard let author = _author else { - assertionFailure() - return - } - try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: .init( - author: author, - authorEntity: notification.entity.account, + author: notification.entity.account, statusViewModel: nil, button: button, barButtonItem: nil @@ -71,16 +62,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for status data provider") return } - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - return .init(objectID: notification.account.objectID) - } - guard let author = _author else { - assertionFailure() - return - } + await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author + account: notification.entity.account ) } // end Task } @@ -322,7 +307,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut await DataSourceFacade.coordinateToProfileScene( provider: self, - user: notification.account.asRecord + account: notification.entity.account ) } // end Task } @@ -494,21 +479,20 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .notification: - assertionFailure("TODO") - default: - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .user(let user): + break + case .account(let account, let relationship): + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } // end Task } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 57360f0c6..115f077c4 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -35,26 +35,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte } switch await statusView.viewModel.header { - case .none: - break - case .reply: - let _replyToAuthor: ManagedObjectRecord? = try? await context.managedObjectContext.perform { - guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil } - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: domain, id: inReplyToAccountID) - request.fetchLimit = 1 - guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } - return .init(objectID: author.objectID) - } - guard let replyToAuthor = _replyToAuthor else { - assertionFailure() - return - } - - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: replyToAuthor - ) + case .none: + break + case .reply: + guard let replyToAccountID = status.entity.inReplyToAccountID else { return } + #warning("TODO: Implement Domain") + await DataSourceFacade.coordinateToProfileScene(provider: self, + domain: "", + accountID: replyToAccountID) case .repost: await DataSourceFacade.coordinateToProfileScene( @@ -469,21 +457,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") return } - + let status = _status.reblog ?? _status - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id) - request.fetchLimit = 1 - guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } - return .init(objectID: author.objectID) - } - guard let author = _author else { - assertionFailure() - return - } - if case .translateStatus = action { DispatchQueue.main.async { if let cell = cell as? StatusTableViewCell { @@ -517,8 +493,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte dependency: self, action: action, menuContext: .init( - author: author, - authorEntity: status.entity.account, + author: status.entity.account, statusViewModel: statusViewModel, button: button, barButtonItem: nil @@ -709,21 +684,23 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .notification: - assertionFailure("TODO") - default: - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .account(let account, _): + await DataSourceFacade.coordinateToProfileScene( + provider: self, + account: account + ) + case .user(_): + assertionFailure("TODO") + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 276500c03..bf3570dbc 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -24,43 +24,37 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid switch item { case .account(let account, relationship: _): await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .hashtag(let tag): - await DataSourceFacade.coordinateToHashtagScene( - provider: self, - tag: tag - ) - case .notification(let notification): - let _status: MastodonStatus? = notification.status - if let status = _status { + case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .status, // remove reblog wrapper + target: .status, // remove reblog wrapper status: status ) - } else { - let _author: ManagedObjectRecord? = notification.account.asRecord - if let author = _author { + case .user(let user): + break + case .hashtag(let tag): + await DataSourceFacade.coordinateToHashtagScene( + provider: self, + tag: tag + ) + case .notification(let notification): + let managedObjectContext = context.managedObjectContext + + let _status: MastodonStatus? = notification.status + if let status = _status { + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + } else { await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author - ) + account: notification.entity.account) } - } - } - } // end Task - } // end func - + } // end Task + } // end func + } } extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController { diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index d3b18240e..30adaeacb 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -13,6 +13,7 @@ import class CoreDataStack.Notification enum DataSourceItem: Hashable { case status(record: MastodonStatus) + @available(*, deprecated, message: "Use .account") case user(record: ManagedObjectRecord) case hashtag(tag: Mastodon.Entity.Tag) case notification(record: MastodonNotification) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 053ce9e76..0ebd0511b 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -295,25 +295,17 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { transition: .show ) } else { - context.managedObjectContext.perform { - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: notification.account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else { - return - } - - let profileViewModel = ProfileViewModel( - context: self.context, - authContext: self.viewModel.authContext, - optionalMastodonUser: mastodonUser - ) - _ = self.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: self, - transition: .show - ) - } + + let profileViewModel = ProfileViewModel( + context: self.context, + authContext: self.viewModel.authContext, + account: notification.account + ) + _ = self.coordinator.present( + scene: .profile(viewModel: profileViewModel), + from: self, + transition: .show + ) } default: break diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index f9a0b1c9d..5033dd60c 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -19,7 +19,7 @@ final class ProfileAboutViewModel { // input let context: AppContext - @Published var user: MastodonUser? + @Published var account: Mastodon.Entity.Account? @Published var isEditing = false @Published var accountForEdit: Mastodon.Entity.Account? @@ -34,23 +34,22 @@ final class ProfileAboutViewModel { init(context: AppContext) { self.context = context - // end init - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.emojis) } - .map { $0.asDictionary } - .assign(to: &$emojiMeta) - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.fields) } - .assign(to: &$fields) - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.createdAt) } - .assign(to: &$createdAt) +#warning("TODO: Implement") +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.emojis) } +// .map { $0.asDictionary } +// .assign(to: &$emojiMeta) +// +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.fields) } +// .assign(to: &$fields) +// +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.createdAt) } +// .assign(to: &$createdAt) Publishers.CombineLatest( $fields, diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index bc3c1dfa8..aba92c9c8 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -128,17 +128,17 @@ extension ProfileHeaderViewController { self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0 } .store(in: &disposeBag) - viewModel.$user + viewModel.$account .receive(on: DispatchQueue.main) - .sink { [weak self] user in - guard let self = self else { return } - guard let user = user else { return } + .sink { [weak self] account in + guard let self, let account else { return } + self.profileHeaderView.prepareForReuse() - self.profileHeaderView.configuration(user: user) + self.profileHeaderView.configuration(account: account) } .store(in: &disposeBag) - viewModel.$relationshipActionOptionSet - .assign(to: \.relationshipActionOptionSet, on: profileHeaderView.viewModel) + viewModel.$relationship + .assign(to: \.relationship, on: profileHeaderView.viewModel) .store(in: &disposeBag) viewModel.$isMyself .assign(to: \.isMyself, on: profileHeaderView.viewModel) @@ -269,35 +269,37 @@ extension ProfileHeaderViewController { // MARK: - ProfileHeaderViewDelegate 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, - previewContext: DataSourceFacade.ImagePreviewContext( - imageView: button.avatarImageView, - containerView: .profileAvatar(profileHeaderView) - ) - ) - } // end Task +#warning("TODO: Implement") +// guard let user = viewModel.user else { return } +// let record: ManagedObjectRecord = .init(objectID: user.objectID) +// +// Task { +// try await DataSourceFacade.coordinateToMediaPreviewScene( +// dependency: self, +// user: record, +// previewContext: DataSourceFacade.ImagePreviewContext( +// imageView: button.avatarImageView, +// containerView: .profileAvatar(profileHeaderView) +// ) +// ) +// } // end Task } 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, - previewContext: DataSourceFacade.ImagePreviewContext( - imageView: imageView, - containerView: .profileBanner(profileHeaderView) - ) - ) - } // end Task +#warning("TODO: Implement") +// guard let account = viewModel.account else { return } +// let record: ManagedObjectRecord = .init(objectID: user.objectID) +// +// Task { +// try await DataSourceFacade.coordinateToMediaPreviewScene( +// dependency: self, +// user: record, +// previewContext: DataSourceFacade.ImagePreviewContext( +// imageView: imageView, +// containerView: .profileBanner(profileHeaderView) +// ) +// ) +// } // end Task } func profileHeaderView( @@ -331,35 +333,39 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { // do nothing break case .follower: - guard let domain = viewModel.user?.domain, - let userID = viewModel.user?.id - else { return } - let followerListViewModel = FollowerListViewModel( - context: context, - authContext: viewModel.authContext, - domain: domain, - userID: userID - ) - _ = coordinator.present( - scene: .follower(viewModel: followerListViewModel), - from: self, - transition: .show - ) +#warning("TODO: Implement") +// guard let domain = viewModel.account.domain, +// let userID = viewModel.account.id +// else { return } +// let followerListViewModel = FollowerListViewModel( +// context: context, +// authContext: viewModel.authContext, +// domain: domain, +// userID: userID +// ) +// _ = coordinator.present( +// scene: .follower(viewModel: followerListViewModel), +// from: self, +// transition: .show +// ) + break case .following: - guard let domain = viewModel.user?.domain, - let userID = viewModel.user?.id - else { return } - let followingListViewModel = FollowingListViewModel( - context: context, - authContext: viewModel.authContext, - domain: domain, - userID: userID - ) - _ = coordinator.present( - scene: .following(viewModel: followingListViewModel), - from: self, - transition: .show - ) +#warning("TODO: Implement") +// guard let domain = viewModel.account.domain, +// let userID = viewModel.account.id +// else { return } +// let followingListViewModel = FollowingListViewModel( +// context: context, +// authContext: viewModel.authContext, +// domain: domain, +// userID: userID +// ) +// _ = coordinator.present( +// scene: .following(viewModel: followingListViewModel), +// from: self, +// transition: .show +// ) + break } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 9301aea50..726ddf715 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -26,8 +26,8 @@ final class ProfileHeaderViewModel { let context: AppContext let authContext: AuthContext - @Published var user: MastodonUser? - @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none + @Published var account: Mastodon.Entity.Account? + @Published var relationship: Mastodon.Entity.Relationship? @Published var isMyself = false @Published var isEditing = false diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift index 8e1693142..b044b0603 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift @@ -7,49 +7,48 @@ import UIKit import Combine -import CoreDataStack +import MastodonSDK extension ProfileHeaderView { - func configuration(user: MastodonUser) { - // header - user.publisher(for: \.header) - .map { _ in user.headerImageURL() } - .assign(to: \.headerImageURL, on: viewModel) - .store(in: &disposeBag) - // avatar - user.publisher(for: \.avatar) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) - // emojiMeta - user.publisher(for: \.emojis) - .map { $0.asDictionary } - .assign(to: \.emojiMeta, on: viewModel) - .store(in: &disposeBag) - // name - user.publisher(for: \.displayName) - .map { _ in user.displayNameWithFallback } - .assign(to: \.name, on: viewModel) - .store(in: &disposeBag) - // username - viewModel.acct = user.acctWithDomain - // bio - user.publisher(for: \.note) - .assign(to: \.note, on: viewModel) - .store(in: &disposeBag) - // 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) + func configuration(account: Mastodon.Entity.Account) { + #warning("TODO: Implement") +// // header +// account.header.publisher +// .assign(to: \.headerImageURL, on: viewModel) +// .store(in: &disposeBag) +// // avatar +// account.avatar.publisher +// .assign(to: \.avatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // emojiMeta +// account.emojis.publisher +// .map { $0.asDictionary } +// .assign(to: \.emojiMeta, on: viewModel) +// .store(in: &disposeBag) +// // name +// account.publisher(for: \.displayName) +// .map { _ in account.displayNameWithFallback } +// .assign(to: \.name, on: viewModel) +// .store(in: &disposeBag) +// // username +// viewModel.acct = account.acctWithDomain +// // bio +// account.publisher(for: \.note) +// .assign(to: \.note, on: viewModel) +// .store(in: &disposeBag) +// // dashboard +// account.publisher(for: \.statusesCount) +// .map { Int($0) } +// .assign(to: \.statusesCount, on: viewModel) +// .store(in: &disposeBag) +// account.publisher(for: \.followingCount) +// .map { Int($0) } +// .assign(to: \.followingCount, on: viewModel) +// .store(in: &disposeBag) +// account.publisher(for: \.followersCount) +// .map { Int($0) } +// .assign(to: \.followersCount, on: viewModel) +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index be0dce929..c0cb383f7 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -14,6 +14,7 @@ import MastodonCore import MastodonUI import MastodonAsset import MastodonLocalization +import MastodonSDK extension ProfileHeaderView { class ViewModel: ObservableObject { @@ -45,15 +46,16 @@ extension ProfileHeaderView { @Published var fields: [MastodonField] = [] - @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none + @Published var relationship: Mastodon.Entity.Relationship? @Published var isRelationshipActionButtonHidden = false @Published var isMyself = false init() { - $relationshipActionOptionSet - .compactMap { $0.highPriorityAction(except: []) } - .map { $0 == .none } - .assign(to: &$isRelationshipActionButtonHidden) +#warning("TODO: Implement") +// $relationshipActionOptionSet +// .compactMap { $0.highPriorityAction(except: []) } +// .map { $0 == .none } +// .assign(to: &$isRelationshipActionButtonHidden) } } } @@ -96,13 +98,14 @@ extension ProfileHeaderView.ViewModel { } .store(in: &disposeBag) // follows you - $relationshipActionOptionSet - .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } - .receive(on: DispatchQueue.main) - .sink { isFollowingBy in - view.followsYouBlurEffectView.isHidden = !isFollowingBy - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// $relationshipActionOptionSet +// .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } +// .receive(on: DispatchQueue.main) +// .sink { isFollowingBy in +// view.followsYouBlurEffectView.isHidden = !isFollowingBy +// } +// .store(in: &disposeBag) // avatar Publishers.CombineLatest4( $avatarImageURL, @@ -117,18 +120,19 @@ extension ProfileHeaderView.ViewModel { )) } .store(in: &disposeBag) - // blur for blocking & blockingBy - $relationshipActionOptionSet - .map { $0.contains(.blocking) || $0.contains(.blockingBy) } - .sink { needsImageOverlayBlurred in - UIView.animate(withDuration: 0.33) { - let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil - view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect - let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil - view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect - } - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// // blur for blocking & blockingBy +// $relationshipActionOptionSet +// .map { $0.contains(.blocking) || $0.contains(.blockingBy) } +// .sink { needsImageOverlayBlurred in +// UIView.animate(withDuration: 0.33) { +// let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil +// view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect +// let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil +// view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect +// } +// } +// .store(in: &disposeBag) // name Publishers.CombineLatest4( $isEditing.removeDuplicates(), @@ -182,17 +186,18 @@ extension ProfileHeaderView.ViewModel { view.bioMetaText.configure(content: metaContent) } .store(in: &disposeBag) - $relationshipActionOptionSet - .receive(on: DispatchQueue.main) - .sink { optionSet in - let isBlocking = optionSet.contains(.blocking) - let isBlockedBy = optionSet.contains(.blockingBy) - let isSuspended = optionSet.contains(.suspended) - let isNeedsHidden = isBlocking || isBlockedBy || isSuspended - - view.bioMetaText.textView.isHidden = isNeedsHidden - } - .store(in: &disposeBag) +#warning("TODO: Implement") + // $relationshipActionOptionSet +// .receive(on: DispatchQueue.main) +// .sink { optionSet in +// let isBlocking = optionSet.contains(.blocking) +// let isBlockedBy = optionSet.contains(.blockingBy) +// let isSuspended = optionSet.contains(.suspended) +// let isNeedsHidden = isBlocking || isBlockedBy || isSuspended +// +// view.bioMetaText.textView.isHidden = isNeedsHidden +// } +// .store(in: &disposeBag) // dashboard $isMyself .receive(on: DispatchQueue.main) @@ -245,22 +250,23 @@ extension ProfileHeaderView.ViewModel { $isRelationshipActionButtonHidden .assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer) .store(in: &disposeBag) - Publishers.CombineLatest3( - $relationshipActionOptionSet, - $isEditing, - $isUpdating - ) - .receive(on: DispatchQueue.main) - .sink { relationshipActionOptionSet, isEditing, isUpdating in - if relationshipActionOptionSet.contains(.edit) { - // check .edit state and set .editing when isEditing - view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) - view.configure(state: isEditing ? .editing : .normal) - } else { - view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) - } - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// Publishers.CombineLatest2( +// $relationshipActionOptionSet, +// $isEditing, +// $isUpdating +// ) +// .receive(on: DispatchQueue.main) +// .sink { relationshipActionOptionSet, isEditing, isUpdating in +// if relationshipActionOptionSet.contains(.edit) { +// // check .edit state and set .editing when isEditing +// view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) +// view.configure(state: isEditing ? .editing : .normal) +// } else { +// view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) +// } +// } +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 92845fa4c..da9dd82e6 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -542,27 +542,3 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate { delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter) } } - -#if DEBUG -import SwiftUI - -struct ProfileHeaderView_Previews: PreviewProvider { - static var previews: some View { - Group { - UIViewPreview(width: 375) { - let banner = ProfileHeaderView() - banner.bannerImageView.image = UIImage(named: "lucas-ludwig") - return banner - } - .previewLayout(.fixed(width: 375, height: 800)) - UIViewPreview(width: 375) { - let banner = ProfileHeaderView() - //banner.bannerImageView.image = UIImage(named: "peter-luo") - return banner - } - .preferredColorScheme(.dark) - .previewLayout(.fixed(width: 375, height: 800)) - } - } -} -#endif diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index ecbaef01e..0d3e3f383 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -16,19 +16,12 @@ final class MeProfileViewModel: ProfileViewModel { @MainActor init(context: AppContext, authContext: AuthContext) { - let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + let me = authContext.mastodonAuthenticationBox.authentication.account() super.init( context: context, authContext: authContext, - optionalMastodonUser: user + account: me ) - - $me - .sink { [weak self] me in - guard let self = self else { return } - self.user = me - } - .store(in: &disposeBag) } override func viewDidLoad() { @@ -37,17 +30,9 @@ final class MeProfileViewModel: ProfileViewModel { Task { do { - - _ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value - - try await context.managedObjectContext.performChanges { - guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { - assertionFailure() - return - } - - self.me = me - } + let account = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value + self.account = account + self.me = account } catch { // do nothing? } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 21ebedee2..f9b6c7ec4 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -203,7 +203,7 @@ extension ProfileViewController { .store(in: &disposeBag) Publishers.CombineLatest4 ( - viewModel.relationshipViewModel.$isSuspended, + viewModel.account.suspended.publisher, profileHeaderViewController.viewModel.$isTitleViewDisplaying, editingAndUpdatingPublisher.eraseToAnyPublisher(), barButtonItemHiddenPublisher.eraseToAnyPublisher() @@ -296,43 +296,43 @@ extension ProfileViewController { private func bindViewModel() { // header +#warning("TODO: Implement") let headerViewModel = profileHeaderViewController.viewModel! - viewModel.$user - .assign(to: \.user, on: headerViewModel) - .store(in: &disposeBag) +// viewModel.$account +// .assign(to: \.account, on: headerViewModel) +// .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: headerViewModel) .store(in: &disposeBag) viewModel.$isUpdating .assign(to: \.isUpdating, on: headerViewModel) .store(in: &disposeBag) - viewModel.relationshipViewModel.$isMyself - .assign(to: \.isMyself, on: headerViewModel) - .store(in: &disposeBag) - viewModel.relationshipViewModel.$optionSet - .map { $0 ?? .none } - .assign(to: \.relationshipActionOptionSet, on: headerViewModel) +// viewModel.relationshipViewModel.$isMyself +// .assign(to: \.isMyself, on: headerViewModel) +// .store(in: &disposeBag) + viewModel.$relationship + .assign(to: \.relationship, on: headerViewModel) .store(in: &disposeBag) viewModel.$accountForEdit .assign(to: \.accountForEdit, on: headerViewModel) .store(in: &disposeBag) - +#warning("TODO: Implement") // timeline - [ - viewModel.postsUserTimelineViewModel, - viewModel.repliesUserTimelineViewModel, - viewModel.mediaUserTimelineViewModel, - ].forEach { userTimelineViewModel in - viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag) - viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag) - viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag) - } +// [ +// viewModel.postsUserTimelineViewModel, +// viewModel.repliesUserTimelineViewModel, +// viewModel.mediaUserTimelineViewModel, +// ].forEach { userTimelineViewModel in +// viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag) +// viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag) +// viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag) +// } // about let aboutViewModel = viewModel.profileAboutViewModel - viewModel.$user - .assign(to: \.user, on: aboutViewModel) - .store(in: &disposeBag) +// viewModel.$account +// .assign(to: \.account, on: aboutViewModel) +// .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: aboutViewModel) .store(in: &disposeBag) @@ -374,7 +374,7 @@ extension ProfileViewController { } .store(in: &disposeBag) Publishers.CombineLatest( - profileHeaderViewController.viewModel.$user, + profileHeaderViewController.viewModel.$account, profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear ) .sink { [weak self] (user, _) in @@ -382,7 +382,7 @@ extension ProfileViewController { Task { _ = try await self.context.apiService.fetchUser( username: user.username, - domain: user.domainFromAcct, + domain: "user.domainFromAcct", authenticationBox: self.authContext.mastodonAuthenticationBox ) } @@ -392,26 +392,23 @@ extension ProfileViewController { private func bindMoreBarButtonItem() { Publishers.CombineLatest( - viewModel.$user, - viewModel.relationshipViewModel.$optionSet + viewModel.$account, + viewModel.$relationship ) - .asyncMap { [weak self] user, relationshipSet -> UIMenu? in - guard let self = self else { return nil } - guard let user = user else { - return nil - } + .asyncMap { [weak self] user, relationship -> UIMenu? in + guard let self, let relationship else { return nil } + let name = user.displayNameWithFallback - let _ = ManagedObjectRecord(objectID: user.objectID) var menuActions: [MastodonMenu.Action] = [ - .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), - .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), + .muteUser(.init(name: name, isMuting: relationship.muting ?? false)), + .blockUser(.init(name: name, isBlocking: relationship.blocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), ] - if let me = self.viewModel?.me, me.following.contains(user) { - let showReblogs = me.showingReblogsBy.contains(user) + if relationship.following { + let showReblogs = relationship.showingReblogs ?? false// me.showingReblogsBy.contains(user) let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) menuActions.insert(.hideReblogs(context), at: 1) } @@ -473,26 +470,6 @@ extension ProfileViewController { .store(in: &disposeBag) } -// private func bindProfileRelationship() { -// -// Publishers.CombineLatest3( -// viewModel.isBlocking.eraseToAnyPublisher(), -// viewModel.isBlockedBy.eraseToAnyPublisher(), -// viewModel.suspended.eraseToAnyPublisher() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isBlocking, isBlockedBy, suspended in -// guard let self = self else { return } -// let isNeedSetHidden = isBlocking || isBlockedBy || suspended -// self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden -// self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden -// self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden -// self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden -// self.viewModel.needsPagePinToTop.value = isNeedSetHidden -// } -// .store(in: &disposeBag) -// } // end func bindProfileRelationship - private func handleMetaPress(_ meta: Meta) { switch meta { case .url(_, _, let url, _): @@ -525,24 +502,19 @@ 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 - ) - guard let activityViewController = _activityViewController else { return } - _ = self.coordinator.present( - scene: .activityViewController( - activityViewController: activityViewController, - sourceView: nil, - barButtonItem: sender - ), - from: self, - transition: .activityViewControllerPresent(animated: true, completion: nil) - ) - } // end Task + let activityViewController = DataSourceFacade.createActivityViewController( + dependency: self, + account: viewModel.account + ) + _ = self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: nil, + barButtonItem: sender + ), + from: self, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) } @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { @@ -556,8 +528,8 @@ extension ProfileViewController { } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { - guard let mastodonUser = viewModel.user else { return } - let mention = "@" + mastodonUser.acct + + let mention = "@" + viewModel.account.acct UITextChecker.learnWord(mention) let composeViewModel = ComposeViewModel( context: context, @@ -683,34 +655,6 @@ extension ProfileViewController: TabBarPagerDataSource { } } -//// MARK: - UIScrollViewDelegate -//extension ProfileViewController: UIScrollViewDelegate { -// -// func scrollViewDidScroll(_ scrollView: UIScrollView) { -// contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y -// let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top -// if scrollView.contentOffset.y < topMaxContentOffsetY { -// self.containerScrollView.contentOffset.y = scrollView.contentOffset.y -// for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers { -// postTimelineView.scrollView?.contentOffset.y = 0 -// } -// contentOffsets.removeAll() -// } else { -// containerScrollView.contentOffset.y = topMaxContentOffsetY -// if viewModel.needsPagePinToTop.value { -// // do nothing -// } else { -// if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer { -// let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y -// customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY -// } -// } -// -// } -// } -// -//} - // MARK: - AuthContextProvider extension ProfileViewController: AuthContextProvider { var authContext: AuthContext { viewModel.authContext } @@ -723,140 +667,140 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { - let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none - +// let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none +#warning("TODO: Implement") // handle edit logic for editable profile // handle relationship logic for non-editable profile - if relationshipActionSet.contains(.edit) { - // do nothing when updating - guard !viewModel.isUpdating else { return } - - guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } - guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } - - let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited - - if isEdited { - // update profile when edited - viewModel.isUpdating = true - Task { @MainActor in - do { - // TODO: handle error - _ = try await viewModel.updateProfileInfo( - headerProfileInfo: profileHeaderViewModel.profileInfoEditing, - aboutProfileInfo: profileAboutViewModel.profileInfoEditing - ) - self.viewModel.isEditing = false - - } catch { - let alertController = UIAlertController( - for: error, - title: L10n.Common.Alerts.EditProfileFailure.title, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) - alertController.addAction(okAction) - self.present(alertController, animated: true) - } - - // finish updating - self.viewModel.isUpdating = false - } // end Task - } else { - // set `updating` then toggle `edit` state - viewModel.isUpdating = true - viewModel.fetchEditProfileInfo() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - defer { - // finish updating - self.viewModel.isUpdating = false - } - switch completion { - case .failure(let error): - let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - _ = self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - case .finished: - // enter editing mode - self.viewModel.isEditing.toggle() - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.viewModel.accountForEdit = response.value - } - .store(in: &disposeBag) - } - } else { - guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } - switch relationshipAction { - case .none: - 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 - ) - } - case .muting: - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, - 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 - ) - } - } - alertController.addAction(unmuteAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocking: - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, - 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 - ) - } - } - alertController.addAction(unblockAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: - break - } - } +// if relationshipActionSet.contains(.edit) { +// // do nothing when updating +// guard !viewModel.isUpdating else { return } +// +// guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } +// guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } +// +// let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited +// +// if isEdited { +// // update profile when edited +// viewModel.isUpdating = true +// Task { @MainActor in +// do { +// // TODO: handle error +// _ = try await viewModel.updateProfileInfo( +// headerProfileInfo: profileHeaderViewModel.profileInfoEditing, +// aboutProfileInfo: profileAboutViewModel.profileInfoEditing +// ) +// self.viewModel.isEditing = false +// +// } catch { +// let alertController = UIAlertController( +// for: error, +// title: L10n.Common.Alerts.EditProfileFailure.title, +// preferredStyle: .alert +// ) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) +// alertController.addAction(okAction) +// self.present(alertController, animated: true) +// } +// +// // finish updating +// self.viewModel.isUpdating = false +// } // end Task +// } else { +// // set `updating` then toggle `edit` state +// viewModel.isUpdating = true +// viewModel.fetchEditProfileInfo() +// .receive(on: DispatchQueue.main) +// .sink { [weak self] completion in +// guard let self = self else { return } +// defer { +// // finish updating +// self.viewModel.isUpdating = false +// } +// switch completion { +// case .failure(let error): +// let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) +// alertController.addAction(okAction) +// _ = self.coordinator.present( +// scene: .alertController(alertController: alertController), +// from: nil, +// transition: .alertController(animated: true, completion: nil) +// ) +// case .finished: +// // enter editing mode +// self.viewModel.isEditing.toggle() +// } +// } receiveValue: { [weak self] response in +// guard let self = self else { return } +// self.viewModel.accountForEdit = response.value +// } +// .store(in: &disposeBag) +// } +// } else { +// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } +// switch relationshipAction { +// case .none: +// 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 +// ) +// } +// case .muting: +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, +// 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 +// ) +// } +// } +// alertController.addAction(unmuteAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocking: +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, +// 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 +// ) +// } +// } +// alertController.addAction(unblockAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: +// break +// } +// } } @@ -885,23 +829,19 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { // MARK: - MastodonMenuDelegate 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, - authorEntity: nil, + author: viewModel.account, statusViewModel: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem ) ) - } // end Task + } } } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 4be56a10e..7a075f6ea 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -34,21 +34,16 @@ class ProfileViewModel: NSObject { let context: AppContext let authContext: AuthContext - @available(*, deprecated, message: "Replace with Account") - @Published var me: MastodonUser? + @Published var me: Mastodon.Entity.Account? + @Published var account: Mastodon.Entity.Account + @Published var relationship: Mastodon.Entity.Relationship? - @available(*, deprecated, message: "Replace with Account") - @Published var user: MastodonUser? - let viewDidAppear = PassthroughSubject() @Published var isEditing = false @Published var isUpdating = false @Published var accountForEdit: Mastodon.Entity.Account? - // output - let relationshipViewModel = RelationshipViewModel() - @Published var userIdentifier: UserIdentifier? = nil @Published var isRelationshipActionButtonHidden: Bool = true @@ -61,10 +56,10 @@ class ProfileViewModel: NSObject { // let needsPagePinToTop = CurrentValueSubject(false) @MainActor - init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account?) { self.context = context self.authContext = authContext - self.user = mastodonUser + self.account = account! self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, authContext: authContext, @@ -87,93 +82,86 @@ class ProfileViewModel: NSObject { super.init() // bind me - self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) - $me - .assign(to: \.me, on: relationshipViewModel) - .store(in: &disposeBag) + self.me = authContext.mastodonAuthenticationBox.authentication.account() // bind user - $user + $account .map { user -> UserIdentifier? in - guard let user = user else { return nil } - return MastodonUserIdentifier(domain: user.domain, userID: user.id) + guard let account, let domain = account.domain else { return nil } + return MastodonUserIdentifier(domain: domain, userID: account.id) } .assign(to: &$userIdentifier) - $user - .assign(to: \.user, on: relationshipViewModel) - .store(in: &disposeBag) - + // bind userIdentifier $userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier) $userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier) $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier) // bind bar button items - relationshipViewModel.$optionSet - .sink { [weak self] optionSet in - guard let self = self else { return } - guard let optionSet = optionSet, !optionSet.contains(.none) else { - self.isReplyBarButtonItemHidden = true - self.isMoreMenuBarButtonItemHidden = true - self.isMeBarButtonItemsHidden = true - return - } - - let isMyself = optionSet.contains(.isMyself) - self.isReplyBarButtonItemHidden = isMyself - self.isMoreMenuBarButtonItemHidden = isMyself - self.isMeBarButtonItemsHidden = !isMyself - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// relationshipViewModel.$optionSet +// .sink { [weak self] optionSet in +// guard let self = self else { return } +// guard let optionSet = optionSet, !optionSet.contains(.none) else { +// self.isReplyBarButtonItemHidden = true +// self.isMoreMenuBarButtonItemHidden = true +// self.isMeBarButtonItemsHidden = true +// return +// } +// +// let isMyself = optionSet.contains(.isMyself) +// self.isReplyBarButtonItemHidden = isMyself +// self.isMoreMenuBarButtonItemHidden = isMyself +// self.isMeBarButtonItemsHidden = !isMyself +// } +// .store(in: &disposeBag) // query relationship - let userRecord = $user.map { user -> ManagedObjectRecord? in - user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } - let pendingRetryPublisher = CurrentValueSubject(1) + #warning("TODO: Implement") +// let pendingRetryPublisher = CurrentValueSubject(1) - // observe friendship - Publishers.CombineLatest( - userRecord, - pendingRetryPublisher - ) - .sink { [weak self] userRecord, _ in - guard let self = self else { return } - guard let userRecord = userRecord else { return } - Task { - do { - let response = try await self.updateRelationship( - record: userRecord, - authenticationBox: self.authContext.mastodonAuthenticationBox - ) - // there are seconds delay after request follow before requested -> following. Query again when needs - guard let relationship = response.value.first else { return } - if relationship.requested == true { - let delay = pendingRetryPublisher.value - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - guard let _ = self else { return } - pendingRetryPublisher.value = min(2 * delay, 60) - } - } - } catch { - } - } // end Task - } - .store(in: &disposeBag) +// // observe friendship +// Publishers.CombineLatest( +// account, +// pendingRetryPublisher +// ) +// .sink { [weak self] account, _ in +// guard let self, let account else { return } +// +// Task { +// do { +// let response = try await self.updateRelationship( +// account: account, +// authenticationBox: self.authContext.mastodonAuthenticationBox +// ) +// // there are seconds delay after request follow before requested -> following. Query again when needs +// guard let relationship = response.value.first else { return } +// if relationship.requested == true { +// let delay = pendingRetryPublisher.value +// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in +// guard let _ = self else { return } +// pendingRetryPublisher.value = min(2 * delay, 60) +// } +// } +// } catch { +// } +// } // end Task +// } +// .store(in: &disposeBag) - let isBlockingOrBlocked = Publishers.CombineLatest( - relationshipViewModel.$isBlocking, - relationshipViewModel.$isBlockingBy - ) - .map { $0 || $1 } - .share() - - Publishers.CombineLatest( - isBlockingOrBlocked, - $isEditing - ) - .map { !$0 && !$1 } - .assign(to: &$isPagingEnabled) +// let isBlockingOrBlocked = Publishers.CombineLatest( +// relationshipViewModel.$isBlocking, +// relationshipViewModel.$isBlockingBy +// ) +// .map { $0 || $1 } +// .share() +// +// Publishers.CombineLatest( +// isBlockingOrBlocked, +// $isEditing +// ) +// .map { !$0 && !$1 } +// .assign(to: &$isPagingEnabled) } @@ -183,21 +171,21 @@ class ProfileViewModel: NSObject { // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { - guard let me else { + guard let me, let domain = me.domain else { return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } let mastodonAuthentication = authContext.mastodonAuthenticationBox.authentication let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) - return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization) + return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization) } private func updateRelationship( - record: ManagedObjectRecord, + account: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { let response = try await context.apiService.relationship( - records: [record], + forAccounts: [account], authenticationBox: authenticationBox ) return response diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index b0a2f9f48..2849e1fe2 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -15,7 +15,7 @@ final class RemoteProfileViewModel: ProfileViewModel { @MainActor init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) let domain = authContext.mastodonAuthenticationBox.domain let authorization = authContext.mastodonAuthenticationBox.userAuthorization @@ -38,62 +38,28 @@ final class RemoteProfileViewModel: ProfileViewModel { break } } receiveValue: { [weak self] response in - guard let self = self else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - self.user = mastodonUser + self?.account = response.value } .store(in: &disposeBag) } @MainActor init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) Task { @MainActor in let response = try await context.apiService.notification( notificationID: notificationID, authenticationBox: authContext.mastodonAuthenticationBox ) - let userID = response.value.account.id - - let _user: MastodonUser? = try await context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) - request.fetchLimit = 1 - return context.managedObjectContext.safeFetch(request).first - } - - if let user = _user { - self.user = user - } else { - _ = try await context.apiService.accountInfo( - domain: authContext.mastodonAuthenticationBox.domain, - userID: userID, - authorization: authContext.mastodonAuthenticationBox.userAuthorization - ) - - let _user: MastodonUser? = try await context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) - request.fetchLimit = 1 - return context.managedObjectContext.safeFetch(request).first - } - - self.user = _user - } + + self.account = response.value.account } // end Task } @MainActor init(context: AppContext, authContext: AuthContext, acct: String){ - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox @@ -116,16 +82,9 @@ final class RemoteProfileViewModel: ProfileViewModel { break } } receiveValue: { [weak self] response in - guard let self = self, let value = response.value else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - self.user = mastodonUser + guard let account = response.value else { return } + + self?.account = account } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift index 468fa5ae6..ae4c3448f 100644 --- a/Mastodon/Scene/Report/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -84,7 +84,7 @@ extension ReportViewController: ReportReasonViewControllerDelegate { let reportResultViewModel = ReportResultViewModel( context: context, authContext: viewModel.authContext, - user: viewModel.user, + account: viewModel.account, isReported: false ) _ = coordinator.present( @@ -160,7 +160,7 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate { let reportResultViewModel = ReportResultViewModel( context: context, authContext: viewModel.authContext, - user: viewModel.user, + account: viewModel.account, isReported: true ) diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index ba71da66f..1a21885f9 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -28,7 +28,7 @@ class ReportViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let status: MastodonStatus? // output @@ -39,17 +39,17 @@ class ReportViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, status: MastodonStatus? ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.status = status self.reportReasonViewModel = ReportReasonViewModel(context: context) self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context) - self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, user: user, status: status) - self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, user: user) + self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, account: account, status: status) + self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, account: account) // end init // setup reason viewModel @@ -57,17 +57,8 @@ class ReportViewModel { reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost } else { Task { @MainActor in - let managedObjectContext = context.managedObjectContext - let _username: String? = try? await managedObjectContext.perform { - let user = user.object(in: managedObjectContext) - return user?.acctWithDomain - } - if let username = _username { - reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(username) - } else { - reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisAccount - } - } // end Task + reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(account.username) + } } // bind server rules @@ -96,73 +87,63 @@ extension ReportViewModel { func report() async throws { guard !isReporting else { return } - let managedObjectContext = context.managedObjectContext - let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform { - guard let user = self.user.object(in: managedObjectContext) else { return nil } - - // the status picker is essential step in report flow - // only check isSkip or not - let statusIDs: [MastodonStatus.ID]? = { - if self.reportStatusViewModel.isSkip { - let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in - return record.id - } - return _id.flatMap { [$0] } ?? [] - } else { - return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in - return record.id - } + let account = self.account + // the status picker is essential step in report flow + // only check isSkip or not + let statusIDs: [MastodonStatus.ID]? = { + if self.reportStatusViewModel.isSkip { + let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in + return record.id } - }() - - // the user comment is essential step in report flow - // only check isSkip or not - let comment: String? = { - let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment - if let comment = _comment, !comment.isEmpty { - return comment - } else { - return nil + return _id.flatMap { [$0] } ?? [] + } else { + return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in + return record.id } - }() - return Mastodon.API.Reports.FileReportQuery( - accountID: user.id, - statusIDs: statusIDs, - comment: comment, - forward: true, - category: { - switch self.reportReasonViewModel.selectReason { + } + }() + + // the user comment is essential step in report flow + // only check isSkip or not + let comment: String? = { + let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment + if let comment = _comment, !comment.isEmpty { + return comment + } else { + return nil + } + }() + let query = Mastodon.API.Reports.FileReportQuery( + accountID: account.id, + statusIDs: statusIDs, + comment: comment, + forward: true, + category: { + switch self.reportReasonViewModel.selectReason { case .dislike: return nil case .spam: return .spam case .violateRule: return .violation case .other: return .other case .none: return nil - } - }(), - ruleIDs: { - switch self.reportReasonViewModel.selectReason { + } + }(), + ruleIDs: { + switch self.reportReasonViewModel.selectReason { case .violateRule: let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted() return ruleIDs default: return nil - } - }() - ) - } - - guard let query = _query else { return } + } + }() + ) do { isReporting = true - #if DEBUG - try await Task.sleep(nanoseconds: .second * 3) - #else let _ = try await context.apiService.report( query: query, authenticationBox: authContext.mastodonAuthenticationBox ) - #endif isReportSuccess = true } catch { isReporting = false diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index d66eadfd7..0b9ab1fba 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -90,7 +90,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: self.viewModel.user + user: self.viewModel.account ) } catch { // handle error @@ -110,7 +110,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: self.viewModel.user + account: self.viewModel.account ) } catch { // handle error @@ -130,7 +130,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: self.viewModel.user + user: self.viewModel.account ) } catch { // handle error diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index de987b512..70853e184 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -23,7 +23,7 @@ class ReportResultViewModel: ObservableObject { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let isReported: Bool var headline: String { @@ -48,24 +48,20 @@ class ReportResultViewModel: ObservableObject { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, isReported: Bool ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.isReported = isReported // end init Task { @MainActor in - guard let user = user.object(in: context.managedObjectContext) else { return } - guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return } - self.relationshipViewModel.user = user - self.relationshipViewModel.me = me - - self.avatarURL = user.avatarImageURL() - self.username = user.acctWithDomain + self.avatarURL = account.avatarImageURL() + self.username = account.username + } // end Task } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index 0bb2e0cef..0c13b32af 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -68,19 +68,9 @@ extension ReportStatusViewModel.State { Task { let maxID = await viewModel.statusFetchedResultsController.records.last?.id - let managedObjectContext = viewModel.context.managedObjectContext - let _userID: MastodonUser.ID? = try await managedObjectContext.perform { - guard let user = viewModel.user.object(in: managedObjectContext) else { return nil } - return user.id - } - guard let userID = _userID else { - await enter(state: Fail.self) - return - } - do { let response = try await viewModel.context.apiService.userTimeline( - accountID: userID, + accountID: viewModel.account.id, maxID: maxID, sinceID: nil, excludeReplies: true, diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 186a2806c..2b3713138 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -24,7 +24,7 @@ class ReportStatusViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let status: MastodonStatus? let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -52,12 +52,12 @@ class ReportStatusViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, status: MastodonStatus? ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.status = status self.statusFetchedResultsController = StatusFetchedResultsController() // end init diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index a4239bbc4..d8f9aa783 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -18,7 +18,7 @@ class ReportSupplementaryViewModel { // Input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let commentContext = ReportItem.CommentContext() @Published var isSkip = false @@ -31,11 +31,11 @@ class ReportSupplementaryViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) { self.context = context self.authContext = authContext - self.user = user + self.account = account // end init Publishers.CombineLatest( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index 7a332ad62..b2fc91781 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -70,10 +70,7 @@ extension SearchResultViewController { status: status ) case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) + assertionFailure() case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( provider: self, diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 2c4973a30..b1150e3f7 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -217,15 +217,8 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate { extension SettingsCoordinator: AboutInstanceViewControllerDelegate { @MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) { - Task { - let user = try await appContext.apiService.fetchUser(username: account.username, domain: authContext.mastodonAuthenticationBox.domain, authenticationBox: authContext.mastodonAuthenticationBox) - - let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, optionalMastodonUser: user) - - _ = await MainActor.run { - sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show) - } - } + let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, account: account) + sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show) } func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) { diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 0b1a9db49..f3b301c4f 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -98,7 +98,13 @@ public struct MastodonAuthentication: Codable, Hashable { let userPredicate = MastodonUser.predicate(domain: domain, id: userID) return MastodonUser.findOrFetch(in: context, matching: userPredicate) } - + + public func account() -> Mastodon.Entity.Account? { + // store accounts +#warning("TODO: Implement") + return nil + } + func updating(instance: Instance) -> Self { copy(instanceObjectIdURI: instance.objectID.uriRepresentation()) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 5e058c258..a1445f71c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -34,19 +34,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ) - } - return response } @@ -171,7 +158,7 @@ extension APIService { extension APIService { public func fetchUser(username: String, domain: String, authenticationBox: MastodonAuthenticationBox) - async throws -> MastodonUser? { + async throws -> Mastodon.Entity.Account? { let query = Mastodon.API.Account.AccountLookupQuery(acct: "\(username)@\(domain)") let authorization = authenticationBox.userAuthorization @@ -182,21 +169,6 @@ extension APIService { authorization: authorization ).singleOutput() - // user - let managedObjectContext = self.backgroundManagedObjectContext - var result: MastodonUser? - try await managedObjectContext.performChanges { - result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ).user - } - - return result + return response.value } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index b3a046bad..dadb34e29 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -145,64 +145,10 @@ extension APIService { return response } - public func toggleShowReblogs( - for user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext - guard let user = user.object(in: managedObjectContext), - let me = authenticationBox.authentication.user(in: managedObjectContext) - else { throw APIError.implicit(.badRequest) } - - let result: Result, Error> - - let oldShowReblogs = me.showingReblogsBy.contains(user) - let newShowReblogs = (oldShowReblogs == false) - - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: user.id, - followQueryType: .follow(query: .init(reblogs: newShowReblogs)), - authorization: authenticationBox.userAuthorization - ).singleOutput() - - result = .success(response) - } catch { - result = .failure(error) - } - - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - - switch result { - case .success(let response): - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isShowingReblogs: oldShowReblogs, by: me) - } - } - - return try result.get() - } - public func toggleShowReblogs( for user: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - let result: Result, Error> - let relationship = try await Mastodon.API.Account.relationships( session: session, domain: authenticationBox.domain, @@ -213,20 +159,14 @@ extension APIService { let oldShowReblogs = relationship?.showingReblogs == true let newShowReblogs = (oldShowReblogs == false) - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: user.id, - followQueryType: .follow(query: .init(reblogs: newShowReblogs)), - authorization: authenticationBox.userAuthorization - ).singleOutput() + let response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init(reblogs: newShowReblogs)), + authorization: authenticationBox.userAuthorization + ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } - - return try result.get() + return response } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index 65f0eb811..d45b33e2e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -14,7 +14,6 @@ import MastodonSDK extension APIService { private struct MastodonMuteContext { - let sourceUserID: MastodonUser.ID let targetUserID: MastodonUser.ID let targetUsername: String let isMuting: Bool @@ -60,33 +59,23 @@ extension APIService { } public func toggleMute( - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + authenticationBox: MastodonAuthenticationBox, + account: Mastodon.Entity.Account ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication + guard let relationship = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: .init(ids: [account.id]), + authorization: authenticationBox.userAuthorization + ).singleOutput().value.first else { throw APIError.implicit(.badRequest) } + + let muteContext = MastodonMuteContext( + targetUserID: account.id, + targetUsername: account.username, + isMuting: relationship.muting ?? false + ) - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } - - let isMuting = user.mutingBy.contains(me) - - // toggle mute state - user.update(isMuting: !isMuting, by: me) - return MastodonMuteContext( - sourceUserID: me.id, - targetUserID: user.id, - targetUsername: user.username, - isMuting: isMuting - ) - } - let result: Result, Error> do { if muteContext.isMuting { @@ -96,7 +85,7 @@ extension APIService { accountID: muteContext.targetUserID, authorization: authenticationBox.userAuthorization ).singleOutput() - try await getMutes(authenticationBox: authenticationBox) + result = .success(response) } else { let response = try await Mastodon.API.Account.mute( @@ -105,38 +94,16 @@ extension APIService { accountID: muteContext.targetUserID, authorization: authenticationBox.userAuthorization ).singleOutput() - try await getMutes(authenticationBox: authenticationBox) + result = .success(response) } } catch { result = .failure(error) } - - try await managedObjectContext.performChanges { - guard let user = user.object(in: managedObjectContext), - let me = authenticationBox.authentication.user(in: managedObjectContext) - else { return } - - switch result { - case .success(let response): - let relationship = response.value - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: relationship, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isMuting: muteContext.isMuting, by: me) - } - } - + let response = try result.get() return response } - + }