diff --git a/Localization/app.json b/Localization/app.json index 734169df0..9d8ce91b4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -222,10 +222,11 @@ "pending": "Pending", "block": "Block", "block_user": "Block %s", - "block_domain": "Block %s", + "block_domain": "Block domain %s", "unblock": "Unblock", "unblock_user": "Unblock %s", "blocked": "Blocked", + "domain_blocked": "Domain Blocked", "mute": "Mute", "mute_user": "Mute %s", "unmute": "Unmute", @@ -603,6 +604,14 @@ "confirm_hide_reblogs": { "title": "Hide Reblogs", "message": "Confirm to hide reblogs" + }, + "confirm_block_domain": { + "title": "Block domain", + "message": "Confirm to block domain %s" + }, + "confirm_unblock_domain": { + "title": "Unblock domain", + "message": "Confirm to unblock domain %s" } }, "accessibility": { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index c8f1f9405..298330219 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -52,4 +52,17 @@ extension DataSourceFacade { ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() } + + static func responseToDomainBlockAction( + dependency: NeedsDependency & AuthContextProvider, + user: ManagedObjectRecord + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + let apiService = dependency.context.apiService + let authBox = dependency.authContext.mastodonAuthenticationBox + + _ = try await apiService.toggleDomainBlock(user: user, authenticationBox: authBox) + } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index d304816d0..c324da73b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -394,8 +394,48 @@ extension DataSourceFacade { try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, user: author) + case .blockDomain(let context): + let title: String + let message: String + let actionTitle: String + + if context.isBlocking { + title = L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title + message = L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(context.domain) + actionTitle = L10n.Common.Controls.Friendship.unblockDomain(context.domain) + } else { + title = L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.title + message = L10n.Common.Alerts.BlockDomain.title(context.domain) + actionTitle = L10n.Common.Alerts.BlockDomain.blockEntireDomain + } + + let alertController = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + let confirmAction = UIAlertAction(title: actionTitle, style: .destructive ) { [weak dependency] _ in + guard let dependency = 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.responseToDomainBlockAction( + dependency: dependency, + user: user + ) + } + } + alertController.addAction(confirmAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) + alertController.addAction(cancelAction) + dependency.present(alertController, animated: true) } - } // end func + } } extension DataSourceFacade { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index be0dce929..fdfef4a2e 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -119,7 +119,7 @@ extension ProfileHeaderView.ViewModel { .store(in: &disposeBag) // blur for blocking & blockingBy $relationshipActionOptionSet - .map { $0.contains(.blocking) || $0.contains(.blockingBy) } + .map { $0.contains(.blocking) || $0.contains(.blockingBy) || $0.contains(.domainBlocking) } .sink { needsImageOverlayBlurred in UIView.animate(withDuration: 0.33) { let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil @@ -185,7 +185,7 @@ extension ProfileHeaderView.ViewModel { $relationshipActionOptionSet .receive(on: DispatchQueue.main) .sink { optionSet in - let isBlocking = optionSet.contains(.blocking) + let isBlocking = optionSet.contains(.blocking) || optionSet.contains(.domainBlocking) let isBlockedBy = optionSet.contains(.blockingBy) let isSuspended = optionSet.contains(.suspended) let isNeedsHidden = isBlocking || isBlockedBy || isSuspended diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index a1c5e3925..6fa6c1372 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -325,6 +325,7 @@ extension ProfileViewController { 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.relationshipViewModel.$isDomainBlocking.assign(to: \.isDomainBlocking, on: userTimelineViewModel).store(in: &disposeBag) } // about @@ -395,16 +396,16 @@ extension ProfileViewController { viewModel.relationshipViewModel.$optionSet ) .asyncMap { [weak self] user, relationshipSet -> UIMenu? in - guard let self = self else { return nil } - guard let user = user else { - return nil - } + guard let self, let user else { return nil } + let name = user.displayNameWithFallback + let domain = user.domainFromAcct 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)), + .blockDomain(.init(domain: domain, isBlocking: self.viewModel.relationshipViewModel.isDomainBlocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), ] @@ -829,6 +830,27 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) + case .domainBlocking: + guard let user = viewModel.user else { return } + let domain = user.domainFromAcct + + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(domain), + 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.responseToDomainBlockAction(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 .blocking: guard let user = viewModel.user else { return } let name = user.displayNameWithFallback diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 67f2b8035..422440020 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -41,11 +41,12 @@ extension UserTimelineViewModel { } .store(in: &disposeBag) - let needsTimelineHidden = Publishers.CombineLatest3( + let needsTimelineHidden = Publishers.CombineLatest4( $isBlocking, $isBlockedBy, - $isSuspended - ).map { $0 || $1 || $2 } + $isSuspended, + $isDomainBlocking + ).map { $0 || $1 || $2 || $3 } Publishers.CombineLatest( statusFetchedResultsController.$records, diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 0c8d634e5..7a4925b8d 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -27,6 +27,7 @@ final class UserTimelineViewModel { @Published var queryFilter: QueryFilter @Published var isBlocking = false + @Published var isDomainBlocking = false @Published var isBlockedBy = false @Published var isSuspended = false diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift index 3bfa519c7..5f1427ef6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+DomainBlock.swift @@ -66,7 +66,31 @@ extension APIService { } .eraseToAnyPublisher() } - + + public func toggleDomainBlock( + user: ManagedObjectRecord, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + guard let originalRelationship = try await relationship(records: [user], authenticationBox: authenticationBox).value.first else { + throw APIError.implicit(.badRequest) + } + + let response: Mastodon.Response.Content + let domainBlocking = originalRelationship.domainBlocking ?? false + + let managedObjectContext = backgroundManagedObjectContext + + guard let _user = user.object(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } + + if domainBlocking { + response = try await unblockDomain(user: _user, authorizationBox: authenticationBox).singleOutput() + } else { + response = try await blockDomain(user: _user, authorizationBox: authenticationBox).singleOutput() + } + + return response + } + func blockDomain( user: MastodonUser, authorizationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 99c061f91..fae0ddc50 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -216,6 +216,8 @@ public enum L10n { public static func blockUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1), fallback: "Block %@") } + /// Domain Blocked + public static let domainBlocked = L10n.tr("Localizable", "Common.Controls.Friendship.DomainBlocked", fallback: "Domain Blocked") /// Edit Info public static let editInfo = L10n.tr("Localizable", "Common.Controls.Friendship.EditInfo", fallback: "Edit Info") /// Follow @@ -241,6 +243,10 @@ public enum L10n { /// Unblock public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Unblock", fallback: "Unblock") /// Unblock %@ + public static func unblockDomain(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockDomain", String(describing: p1), fallback: "Unblock %@") + } + /// Unblock %@ public static func unblockUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Friendship.UnblockUser", String(describing: p1), fallback: "Unblock %@") } @@ -953,6 +959,10 @@ public enum L10n { public static let followsYou = L10n.tr("Localizable", "Scene.Profile.Header.FollowsYou", fallback: "Follows You") } public enum RelationshipActionAlert { + public enum ConfirmBlockDomain { + /// Block Domain + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.Title", fallback: "Block Domain") + } public enum ConfirmBlockUser { /// Confirm to block %@ public static func message(_ p1: Any) -> String { @@ -981,6 +991,14 @@ public enum L10n { /// Show Reblogs public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title", fallback: "Show Reblogs") } + public enum ConfirmUnblockDomain { + /// Confirm to unblock domain %@ + public static func message(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Message", String(describing: p1), fallback: "Confirm to unblock domain %@") + } + /// Unblock Domain + public static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Title", fallback: "Unblock Domain") + } public enum ConfirmUnblockUser { /// Confirm to unblock %@ public static func message(_ p1: Any) -> String { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 2faf5ebb7..bd1bfc949 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -73,6 +73,7 @@ Please check your internet connection."; "Common.Controls.Friendship.BlockDomain" = "Block %@"; "Common.Controls.Friendship.BlockUser" = "Block %@"; "Common.Controls.Friendship.Blocked" = "Blocked"; +"Common.Controls.Friendship.DomainBlocked" = "Domain Blocked"; "Common.Controls.Friendship.EditInfo" = "Edit Info"; "Common.Controls.Friendship.Follow" = "Follow"; "Common.Controls.Friendship.Following" = "Following"; @@ -85,6 +86,7 @@ Please check your internet connection."; "Common.Controls.Friendship.ShowReblogs" = "Show Reblogs"; "Common.Controls.Friendship.Unblock" = "Unblock"; "Common.Controls.Friendship.UnblockUser" = "Unblock %@"; +"Common.Controls.Friendship.UnblockDomain" = "Unblock %@"; "Common.Controls.Friendship.Unmute" = "Unmute"; "Common.Controls.Friendship.UnmuteUser" = "Unmute %@"; "Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post"; @@ -336,6 +338,9 @@ uploaded to Mastodon."; "Scene.Profile.Header.FollowsYou" = "Follows You"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@"; "Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmBlockDomain.Title" = "Block Domain"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Message" = "Confirm to unblock domain %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.Title" = "Unblock Domain"; "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs"; "Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirm to mute %@"; @@ -563,4 +568,4 @@ uploaded to Mastodon."; "Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts."; "Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers"; "Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; -"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; \ No newline at end of file +"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index 65231c51d..72f2877a9 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -59,7 +59,8 @@ extension MastodonMenu { case deleteStatus case editStatus case followUser(FollowUserActionContext) - + case blockDomain(BlockDomainActionContext) + func build(delegate: MastodonMenuDelegate) -> LabeledAction { switch self { case .hideReblogs(let context): @@ -194,14 +195,29 @@ extension MastodonMenu { image = UIImage(systemName: "person.fill.badge.plus") } let action = LabeledAction(title: title, image: image) { [weak delegate] in - guard let delegate = delegate else { return } + guard let delegate else { return } delegate.menuAction(self) } return action + case .blockDomain(let context): + let title: String + let image: UIImage? + if context.isBlocking { + title = L10n.Common.Controls.Actions.unblockDomain(context.domain) + image = UIImage(systemName: "hand.raised.slash.fill") + } else { + title = L10n.Common.Controls.Actions.blockDomain(context.domain) + image = UIImage(systemName: "hand.raised.fill") + } + let action = LabeledAction(title: title, image: image) { [weak delegate] in + guard let delegate else { return } - } // end switch - } // end func build - } // end enum Action + delegate.menuAction(self) + } + return action + } + } + } } extension MastodonMenu { @@ -275,4 +291,14 @@ extension MastodonMenu { self.isFollowing = isFollowing } } + + public struct BlockDomainActionContext { + public let domain: String + public let isBlocking: Bool + + public init(domain: String, isBlocking: Bool) { + self.domain = domain + self.isBlocking = isBlocking + } + } } diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 20f720d20..b8a579199 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -28,6 +28,7 @@ public enum RelationshipAction: Int, CaseIterable { case edit case editing case updating + case domainBlocking public var option: RelationshipActionOptionSet { return RelationshipActionOptionSet(rawValue: 1 << rawValue) @@ -60,7 +61,8 @@ public struct RelationshipActionOptionSet: OptionSet { public static let updating = RelationshipAction.updating.option public static let showReblogs = RelationshipAction.showReblogs.option public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] - + public static let domainBlocking = RelationshipAction.domainBlocking.option + public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? { let set = subtracting(except) for action in RelationshipAction.allCases.reversed() where set.contains(action.option) { @@ -92,6 +94,7 @@ public struct RelationshipActionOptionSet: OptionSet { case .editing: return L10n.Common.Controls.Actions.done case .updating: return " " case .showReblogs: return " " + case .domainBlocking: return L10n.Common.Controls.Friendship.domainBlocked } } } @@ -119,7 +122,8 @@ public final class RelationshipViewModel { @Published public var isBlocking = false @Published public var isBlockingBy = false @Published public var isSuspended = false - + @Published public var isDomainBlocking = false + public init() { Publishers.CombineLatest3( $user, @@ -171,9 +175,7 @@ extension RelationshipViewModel { extension RelationshipViewModel { private func update(user: MastodonUser?, me: MastodonUser?) { - guard let user = user, - let me = me - else { + guard let user, let me else { reset() return } @@ -188,6 +190,7 @@ extension RelationshipViewModel { self.isBlocking = optionSet.contains(.blocking) self.isSuspended = optionSet.contains(.suspended) self.showReblogs = optionSet.contains(.showReblogs) + self.isDomainBlocking = optionSet.contains(.domainBlocking) self.optionSet = optionSet } @@ -201,6 +204,7 @@ extension RelationshipViewModel { isBlocking = false optionSet = nil showReblogs = false + isDomainBlocking = false } } @@ -220,6 +224,7 @@ extension RelationshipViewModel { let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) let isShowingReblogs = me.showingReblogsBy.contains(user) + let isDomainBlocking = user.domainBlockingBy.contains(me) var optionSet: RelationshipActionOptionSet = [.follow] @@ -263,6 +268,10 @@ extension RelationshipViewModel { optionSet.insert(.showReblogs) } + if isDomainBlocking { + optionSet.insert(.domainBlocking) + } + return optionSet } }