diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 3f8693476..c8f1f9405 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToUserBlockAction( @@ -29,5 +30,26 @@ extension DataSourceFacade { authenticationBox: authBox ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } // end func + } + + static func responseToUserBlockAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + let apiService = dependency.context.apiService + let authBox = dependency.authContext.mastodonAuthenticationBox + + _ = try await apiService.toggleBlock( + user: user, + authenticationBox: authBox + ) + + try await dependency.context.apiService.getBlocked( + authenticationBox: authBox + ) + dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 88503ae7b..6fe0005a0 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -25,7 +25,22 @@ extension DataSourceFacade { authenticationBox: dependency.authContext.mastodonAuthenticationBox ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } // end func + } + + static func responseToUserFollowAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + _ = try await dependency.context.apiService.toggleFollow( + user: user, + authenticationBox: dependency.authContext.mastodonAuthenticationBox + ) + dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + } + } extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index 8b1a5c84d..a90bdd0a8 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -64,16 +64,52 @@ extension DataSourceFacade { break //no-op } } -} -extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider { - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) { - Task { - try await DataSourceFacade.responseToUserViewButtonAction( - dependency: self, - user: user.asRecord, - buttonState: state - ) + static func responseToUserViewButtonAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account, + buttonState: UserView.ButtonState + ) async throws { + switch buttonState { + case .follow: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id) + + case .request: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id) + case .unfollow: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id }) + case .blocked: + try await DataSourceFacade.responseToUserBlockAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id) + + case .pending: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == user.id }) + case .none, .loading: + break //no-op } } } diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 6deeb0a2a..c83838492 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -13,6 +13,7 @@ import MastodonLocalization import MastodonMeta import MastodonCore import Meta +import MastodonSDK extension UserView { public func configure(user: MastodonUser, delegate: UserViewDelegate?) { @@ -63,4 +64,8 @@ extension UserView { .assign(to: \.authorVerifiedLink, on: viewModel) .store(in: &disposeBag) } + + func configure(with account: Mastodon.Entity.Account) { + //TODO: Implement + } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 526e74ab3..d2f62e2fc 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -9,6 +9,8 @@ import UIKit import CoreDataStack import MastodonUI import Combine +import MastodonCore +import MastodonSDK extension UserTableViewCell { final class ViewModel { @@ -72,5 +74,27 @@ extension UserTableViewCell { self.delegate = delegate } - +} + +extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider { + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) { + Task { + try await DataSourceFacade.responseToUserViewButtonAction( + dependency: self, + user: user.asRecord, + buttonState: state + ) + } + } + + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) { + Task { + try await DataSourceFacade.responseToUserViewButtonAction( + dependency: self, + user: user, + buttonState: state + ) + } + } + } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 45543dd1f..f50509628 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -148,7 +148,98 @@ extension APIService { let response = try result.get() return response } - + + public func toggleBlock( + user: Mastodon.Entity.Account, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + fatalError("Not implemented yet") + +// let managedObjectContext = backgroundManagedObjectContext +// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { +// let authentication = authenticationBox.authentication +// +// guard +// let user = user.object(in: managedObjectContext), +// let me = authentication.user(in: managedObjectContext) +// else { +// throw APIError.implicit(.badRequest) +// } +// +// let isBlocking = user.blockingBy.contains(me) +// let isFollowing = user.followingBy.contains(me) +// // toggle block state +// user.update(isBlocking: !isBlocking, by: me) +// // update follow state implicitly +// if !isBlocking { +// // will do block action. set to unfollow +// user.update(isFollowing: false, by: me) +// } +// +// return MastodonBlockContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// targetUsername: user.username, +// isBlocking: isBlocking, +// isFollowing: isFollowing +// ) +// } +// +// let result: Result, Error> +// do { +// if blockContext.isBlocking { +// let response = try await Mastodon.API.Account.unblock( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } else { +// let response = try await Mastodon.API.Account.block( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } +// } catch { +// result = .failure(error) +// } +// +// try await managedObjectContext.performChanges { +// let authentication = authenticationBox.authentication +// +// guard +// let user = user.object(in: managedObjectContext), +// let me = authentication.user(in: managedObjectContext) +// else { return } +// +// +// switch result { +// case .success(let response): +// let relationship = response.value +// Persistence.MastodonUser.update( +// mastodonUser: user, +// context: Persistence.MastodonUser.RelationshipContext( +// entity: relationship, +// me: me, +// networkDate: response.networkDate +// ) +// ) +// case .failure: +// // rollback +// user.update(isBlocking: blockContext.isBlocking, by: me) +// user.update(isFollowing: blockContext.isFollowing, by: me) +// } +// } +// +// let response = try result.get() +// return response + } + + } extension MastodonUser { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 8decfe632..027dae1cf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -38,11 +38,11 @@ extension APIService { let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } guard let user = user.object(in: managedObjectContext) else { return nil } - + let isFollowing = user.followingBy.contains(me) let isPending = user.followRequestedBy.contains(me) let needsUnfollow = isFollowing || isPending - + if needsUnfollow { // unfollow user.update(isFollowing: false, by: me) @@ -66,11 +66,11 @@ extension APIService { ) return context } - + guard let followContext = _followContext else { throw APIError.implicit(.badRequest) } - + // request follow or unfollow let result: Result, Error> do { @@ -85,13 +85,13 @@ extension APIService { } catch { result = .failure(error) } - + // update friendship state try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext), let user = user.object(in: managedObjectContext) else { return } - + switch result { case .success(let response): Persistence.MastodonUser.update( @@ -108,11 +108,105 @@ extension APIService { user.update(isFollowRequested: followContext.isPending, by: me) } } - + let response = try result.get() return response } + public func toggleFollow( + user: Mastodon.Entity.Account, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + fatalError("Not implemented yet") + + /** + 1. Get relation between me and user + 2. check if I follow them: + if so: unfollow + if not: follow + 3. return result of 2. + + */ + +// let managedObjectContext = backgroundManagedObjectContext +// let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } +// guard let user = user.object(in: managedObjectContext) else { return nil } +// +// let isFollowing = user.followingBy.contains(me) +// let isPending = user.followRequestedBy.contains(me) +// let needsUnfollow = isFollowing || isPending +// +// if needsUnfollow { +// // unfollow +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: false, by: me) +// } else { +// // follow +// if user.locked { +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: true, by: me) +// } else { +// user.update(isFollowing: true, by: me) +// user.update(isFollowRequested: false, by: me) +// } +// } +// let context = MastodonFollowContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// isFollowing: isFollowing, +// isPending: isPending, +// needsUnfollow: needsUnfollow +// ) +// return context +// } +// +// guard let followContext = _followContext else { +// throw APIError.implicit(.badRequest) +// } +// +// // request follow or unfollow +// let result: Result, Error> +// do { +// let response = try await Mastodon.API.Account.follow( +// session: session, +// domain: authenticationBox.domain, +// accountID: followContext.targetUserID, +// followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } catch { +// result = .failure(error) +// } +// +// // update friendship state +// try await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext), +// let user = user.object(in: managedObjectContext) +// else { return } +// +// switch result { +// case .success(let response): +// Persistence.MastodonUser.update( +// mastodonUser: user, +// context: Persistence.MastodonUser.RelationshipContext( +// entity: response.value, +// me: me, +// networkDate: response.networkDate +// ) +// ) +// case .failure: +// // rollback +// user.update(isFollowing: followContext.isFollowing, by: me) +// user.update(isFollowRequested: followContext.isPending, by: me) +// } +// } +// +// let response = try result.get() +// return response + } + public func toggleShowReblogs( for user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index 4127ed4d4..c4c2edd9a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -13,6 +13,7 @@ import MastodonCore import MastodonMeta import MastodonAsset import MastodonLocalization +import MastodonSDK extension UserView { public final class ViewModel: ObservableObject { @@ -26,6 +27,7 @@ extension UserView { @Published public var authorFollowers: Int? @Published public var authorVerifiedLink: String? @Published public var user: MastodonUser? + @Published public var account: Mastodon.Entity.Account? } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index cd07b3004..186470b4a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -12,9 +12,11 @@ import MastodonAsset import MastodonLocalization import os import CoreDataStack +import MastodonSDK public protocol UserViewDelegate: AnyObject { func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) } public final class UserView: UIView { @@ -251,9 +253,12 @@ public extension UserView { } } - @objc private func didTapButton() { - guard let user = viewModel.user else { return } - delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) + @objc private func didTapFollowButton() { + if let user = viewModel.user { + delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) + } else if let account = viewModel.account { + delegate?.userView(self, didTapButtonWith: currentButtonState, for: account) + } } func setButtonState(_ state: ButtonState) { @@ -310,7 +315,8 @@ public extension UserView { followButton.configuration?.baseBackgroundColor = .clear } - followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + followButton.addTarget(self, action: #selector(didTapFollowButton), for: .touchUpInside) followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15)) } } +