192-profile-about

# Conflicts:
#	Mastodon/Protocol/Provider/DataSourceFacade+Status.swift
#	Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift
#	Mastodon/Scene/Profile/ProfileViewController.swift
#	MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift
This commit is contained in:
Nathan Mattes 2024-01-02 12:15:15 +01:00
commit 96fddaef27
16 changed files with 214 additions and 69 deletions

View File

@ -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": {

View File

@ -4623,7 +4623,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2023.17;
MARKETING_VERSION = 2024.1;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4654,7 +4654,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2023.17;
MARKETING_VERSION = 2024.1;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4844,7 +4844,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2023.17;
MARKETING_VERSION = 2024.1;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -5140,7 +5140,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2023.17;
MARKETING_VERSION = 2024.1;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -34,4 +34,19 @@ extension DataSourceFacade {
return response.value
}
static func responseToDomainBlockAction(
dependency: NeedsDependency & AuthContextProvider,
account: Mastodon.Entity.Account
) async throws -> Mastodon.Entity.Empty {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
let apiService = dependency.context.apiService
let authBox = dependency.authContext.mastodonAuthenticationBox
let response = try await apiService.toggleDomainBlock(account: account, authenticationBox: authBox)
return response.value
}
}

View File

@ -367,8 +367,42 @@ extension DataSourceFacade {
case .followUser(_):
try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
account: menuContext.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 else { return }
Task {
try await DataSourceFacade.responseToDomainBlockAction(
dependency: dependency,
account: menuContext.author
)
}
}
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 {

View File

@ -133,7 +133,7 @@ extension ProfileHeaderView.ViewModel {
.compactMap { relationship in
guard let relationship else { return false }
return relationship.blocking || relationship.blockedBy
return relationship.blocking || relationship.blockedBy || relationship.domainBlocking
}
.sink { needsImageOverlayBlurred in
UIView.animate(withDuration: 0.33) {
@ -203,9 +203,10 @@ extension ProfileHeaderView.ViewModel {
guard let relationship else { return nil }
let isBlocking = relationship.blocking
let isBlocking = relationship.blocking || relationship.domainBlocking
let isBlockedBy = relationship.blockedBy
let isSuspended = account.suspended ?? false
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
return isNeedsHidden

View File

@ -791,6 +791,33 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
} else if relationship.domainBlocking {
guard let domain = account.domain else { return }
let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.title,
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockDomain.message(domain),
preferredStyle: .alert
)
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblockDomain(domain), style: .default) { [weak self] _ in
guard let self else { return }
Task {
_ = try await DataSourceFacade.responseToDomainBlockAction(
dependency: self,
account: account
)
guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first else { return }
self.viewModel.relationship = newRelationship
}
}
alertController.addAction(unblockAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true))
} else if relationship.muting {
let name = account.displayNameWithFallback
@ -877,6 +904,7 @@ extension ProfileViewController: MastodonMenuDelegate {
switch action {
case .muteUser(_),
.blockUser(_),
.blockDomain(_),
.hideReblogs(_):
Task {
try await DataSourceFacade.responseToMenuAction(

View File

@ -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,

View File

@ -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

View File

@ -66,72 +66,64 @@ extension APIService {
}
.eraseToAnyPublisher()
}
public func toggleDomainBlock(
account: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Empty> {
guard let originalRelationship = try await relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first else {
throw APIError.implicit(.badRequest)
}
let response: Mastodon.Response.Content<Mastodon.Entity.Empty>
let domainBlocking = originalRelationship.domainBlocking
if domainBlocking {
response = try await unblockDomain(account: account, authorizationBox: authenticationBox)
} else {
response = try await blockDomain(account: account, authorizationBox: authenticationBox)
}
return response
}
func blockDomain(
user: MastodonUser,
account: Mastodon.Entity.Account,
authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Empty> {
let authorization = authorizationBox.userAuthorization
return Mastodon.API.DomainBlock.blockDomain(
guard let domain = account.domainFromAcct else {
throw APIError.implicit(.badRequest)
}
let result = try await Mastodon.API.DomainBlock.blockDomain(
domain: authorizationBox.domain,
blockDomain: user.domainFromAcct,
blockDomain: domain,
session: session,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> in
self.backgroundManagedObjectContext.performChanges {
let requestMastodonUserRequest = MastodonUser.sortedFetchRequest
requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: authorizationBox.domain, id: authorizationBox.userID)
requestMastodonUserRequest.fetchLimit = 1
guard let requestMastodonUser = self.backgroundManagedObjectContext.safeFetch(requestMastodonUserRequest).first else { return }
user.update(isDomainBlocking: true, by: requestMastodonUser)
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Empty> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
).singleOutput()
return result
}
func unblockDomain(
user: MastodonUser,
account: Mastodon.Entity.Account,
authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Empty> {
let authorization = authorizationBox.userAuthorization
return Mastodon.API.DomainBlock.unblockDomain(
guard let domain = account.domainFromAcct else {
throw APIError.implicit(.badRequest)
}
let result = try await Mastodon.API.DomainBlock.unblockDomain(
domain: authorizationBox.domain,
blockDomain: user.domainFromAcct,
blockDomain: domain,
session: session,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> in
self.backgroundManagedObjectContext.performChanges {
let requestMastodonUserRequest = MastodonUser.sortedFetchRequest
requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: authorizationBox.domain, id: authorizationBox.userID)
requestMastodonUserRequest.fetchLimit = 1
guard let requestMastodonUser = self.backgroundManagedObjectContext.safeFetch(requestMastodonUserRequest).first else { return }
user.update(isDomainBlocking: false, by: requestMastodonUser)
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Empty> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
).singleOutput()
return result
}
}

View File

@ -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 {

View File

@ -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";
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";

View File

@ -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
}
}
}

View File

@ -4,6 +4,7 @@ import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
import MastodonCore
import MastodonLocalization
struct FollowersCountWidgetProvider: IntentTimelineProvider {
@ -70,6 +71,9 @@ struct FollowersCountWidget: Widget {
private extension FollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
Task {
AuthenticationServiceProvider.shared.restore()
guard
let authBox = WidgetExtension.appContext
.authenticationService

View File

@ -4,6 +4,7 @@ import WidgetKit
import SwiftUI
import MastodonSDK
import MastodonLocalization
import MastodonCore
struct HashtagWidgetProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> HashtagWidgetTimelineEntry {
@ -24,6 +25,8 @@ struct HashtagWidgetProvider: IntentTimelineProvider {
extension HashtagWidgetProvider {
func loadMostRecentHashtag(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void ) {
AuthenticationServiceProvider.shared.restore()
guard
let authBox = WidgetExtension.appContext
.authenticationService

View File

@ -5,6 +5,7 @@ import SwiftUI
import Intents
import MastodonSDK
import MastodonLocalization
import MastodonCore
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> LatestFollowersEntry {
@ -77,6 +78,9 @@ struct LatestFollowersWidget: Widget {
private extension LatestFollowersWidgetProvider {
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
Task { @MainActor in
AuthenticationServiceProvider.shared.restore()
guard
let authBox = WidgetExtension.appContext
.authenticationService

View File

@ -4,6 +4,7 @@ import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
import MastodonCore
import MastodonLocalization
struct MultiFollowersCountWidgetProvider: IntentTimelineProvider {
@ -70,6 +71,9 @@ struct MultiFollowersCountWidget: Widget {
private extension MultiFollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
Task {
AuthenticationServiceProvider.shared.restore()
guard
let authBox = WidgetExtension.appContext
.authenticationService