diff --git a/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewController.swift b/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewController.swift index d88990cce..081733915 100644 --- a/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewController.swift +++ b/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewController.swift @@ -1,5 +1,6 @@ // Copyright © 2024 Mastodon gGmbH. All rights reserved. +import Combine import UIKit import SwiftUI import MastodonSDK @@ -7,14 +8,24 @@ import MastodonCore import MastodonLocalization import MastodonAsset -final class PrivacySafetyViewController: UIHostingController, NeedsDependency { - weak var context: AppContext! - weak var coordinator: SceneCoordinator! - - init(context: AppContext, coordinator: SceneCoordinator) { - self.context = context - self.coordinator = coordinator - super.init(rootView: PrivacySafetyView(viewModel: PrivacySafetyViewModel())) +final class PrivacySafetyViewController: UIHostingController { + private let viewModel: PrivacySafetyViewModel + private var disposeBag = [AnyCancellable]() + + init(appContext: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) { + self.viewModel = PrivacySafetyViewModel( + appContext: appContext, authContext: authContext, coordinator: coordinator + ) + super.init( + rootView: PrivacySafetyView( + viewModel: self.viewModel + ) + ) + self.viewModel.onDismiss.receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.dismiss(animated: true) + } + .store(in: &disposeBag) } @MainActor required dynamic init?(coder aDecoder: NSCoder) { @@ -72,6 +83,7 @@ struct PrivacySafetyView: View { } } .onAppear(perform: viewModel.viewDidAppear) + .onDisappear(perform: viewModel.saveSettings) } } diff --git a/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewModel.swift b/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewModel.swift index af80a3317..67d964a3c 100644 --- a/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewModel.swift +++ b/Mastodon/Scene/Settings/Privacy and Safety/PrivacySafetyViewModel.swift @@ -1,5 +1,6 @@ // Copyright © 2024 Mastodon gGmbH. All rights reserved. +import Combine import Foundation import MastodonLocalization import MastodonCore @@ -23,10 +24,42 @@ class PrivacySafetyViewModel: ObservableObject { return L10n.Scene.Settings.PrivacySafety.DefaultPostVisibility.onlyPeopleMentioned } } + + static func from(_ privacy: Mastodon.Entity.Source.Privacy) -> Self { + switch privacy { + case .public: + return .public + case .unlisted: + return .followersOnly + case .private, .direct: + return .onlyPeopleMentioned + case ._other(_): + return .public + } + } + + func toPrivacy() -> Mastodon.Entity.Source.Privacy { + switch self { + case .public: + return .public + case .followersOnly: + return .unlisted + case .onlyPeopleMentioned: + return .private + } + } } - weak var appContext: AppContext? - + private var appContext: AppContext? + private var authContext: AuthContext? + private var coordinator: SceneCoordinator? + + init(appContext: AppContext?, authContext: AuthContext?, coordinator: SceneCoordinator?) { + self.appContext = appContext + self.authContext = authContext + self.coordinator = coordinator + } + @Published var preset: Preset = .openPublic { didSet { applyPreset(preset) } } @@ -52,6 +85,7 @@ class PrivacySafetyViewModel: ObservableObject { private var doNotEvaluate = true @Published var isInitialized = false + let onDismiss = PassthroughSubject() func viewDidAppear() { doNotEvaluate = false @@ -96,23 +130,63 @@ extension PrivacySafetyViewModel { func loadSettings() { Task { @MainActor in + guard let appContext, let authContext else { + return dismiss() + } + let domain = authContext.mastodonAuthenticationBox.domain + let userAuthorization = authContext.mastodonAuthenticationBox.userAuthorization + let account = try await appContext.apiService.accountVerifyCredentials( + domain: domain, + authorization: userAuthorization + ).singleOutput().value + + if let privacy = account.source?.privacy { + visibility = .from(privacy) + } + + manuallyApproveFollowRequests = account.locked == true + showFollowersAndFollowing = account.hideCollections == false + suggestMyAccountToOthers = account.discoverable == true + appearInSearches = account.indexable == true + isInitialized = true } } func saveSettings() { Task { + guard let appContext, let authContext else { + return + } + + let domain = authContext.mastodonAuthenticationBox.domain + let userAuthorization = authContext.mastodonAuthenticationBox.userAuthorization + let _ = try await appContext.apiService.accountUpdateCredentials( + domain: domain, + query: .init( + discoverable: suggestMyAccountToOthers, + locked: manuallyApproveFollowRequests, + source: .withPrivacy(visibility.toPrivacy()), + indexable: appearInSearches, + hideCollections: !showFollowersAndFollowing + ), + authorization: userAuthorization + ).value } } + + func dismiss() { + onDismiss.send(()) + } } // Preset Rules Definition extension PrivacySafetyViewModel { static let openPublic: PrivacySafetyViewModel = { - let vm = PrivacySafetyViewModel() + let vm = PrivacySafetyViewModel(appContext: nil, authContext: nil, coordinator: nil) vm.visibility = .public vm.manuallyApproveFollowRequests = false vm.showFollowersAndFollowing = true @@ -122,7 +196,7 @@ extension PrivacySafetyViewModel { }() static let privateRestricted: PrivacySafetyViewModel = { - let vm = PrivacySafetyViewModel() + let vm = PrivacySafetyViewModel(appContext: nil, authContext: nil, coordinator: nil) vm.visibility = .followersOnly vm.manuallyApproveFollowRequests = true vm.showFollowersAndFollowing = false diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 969e50803..7c98cdbbf 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -72,7 +72,11 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { navigationController.pushViewController(notificationViewController, animated: true) case .privacySafety: - let privacySafetyViewController = PrivacySafetyViewController(context: appContext, coordinator: sceneCoordinator) + let privacySafetyViewController = PrivacySafetyViewController( + appContext: appContext, + authContext: authContext, + coordinator: sceneCoordinator + ) navigationController.pushViewController(privacySafetyViewController, animated: true) case .serverDetails(let domain): let serverDetailsViewController = ServerDetailsViewController(domain: domain, appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift index 516958553..0240b8794 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift @@ -159,7 +159,8 @@ extension Mastodon.API.Account { public let source: Mastodon.Entity.Source? public let fieldsAttributes: [Mastodon.Entity.Field]? public let indexable: Bool? - + public let hideCollections: Bool? + enum CodingKeys: String, CodingKey { case discoverable case bot @@ -172,6 +173,7 @@ extension Mastodon.API.Account { case source case fieldsAttributes = "fields_attributes" case indexable + case hideCollections = "hide_collections" } public init( @@ -184,7 +186,8 @@ extension Mastodon.API.Account { locked: Bool? = nil, source: Mastodon.Entity.Source? = nil, fieldsAttributes: [Mastodon.Entity.Field]? = nil, - indexable: Bool? = nil + indexable: Bool? = nil, + hideCollections: Bool? = nil ) { self.discoverable = discoverable self.bot = bot @@ -196,6 +199,7 @@ extension Mastodon.API.Account { self.source = source self.fieldsAttributes = fieldsAttributes self.indexable = indexable + self.hideCollections = hideCollections } var contentType: String? { @@ -209,6 +213,7 @@ extension Mastodon.API.Account { var body: Data? { var data = Data() + hideCollections.flatMap { data.append(Data.multipart(key: "hide_collections", value: $0)) } discoverable.flatMap { data.append(Data.multipart(key: "discoverable", value: $0)) } bot.flatMap { data.append(Data.multipart(key: "bot", value: $0)) } displayName.flatMap { data.append(Data.multipart(key: "display_name", value: $0)) } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Preferences.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Preferences.swift deleted file mode 100644 index a93cc20af..000000000 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Preferences.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Marcus Kida on 29.05.24. -// - -import Foundation diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 29b73763c..0044dfe2c 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -37,6 +37,8 @@ extension Mastodon.Entity { public let locked: Bool public let emojis: [Emoji] public let discoverable: Bool? + public let hideCollections: Bool? + public let indexable: Bool? // Statistical public let createdAt: Date @@ -71,6 +73,8 @@ extension Mastodon.Entity.Account: Codable { case locked case emojis case discoverable + case hideCollections = "hide_collections" + case indexable case createdAt = "created_at" case lastStatusAt = "last_status_at" diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Source.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Source.swift index bc9c60f7a..2a7952b0b 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Source.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Source.swift @@ -36,6 +36,10 @@ extension Mastodon.Entity { case language case followRequestsCount = "follow_requests_count" } + + public static func withPrivacy(_ privacy: Privacy) -> Self { + Source(note: "", fields: nil, privacy: privacy, sensitive: nil, language: nil, followRequestsCount: nil) + } } }