2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

IOS-168: Implement Privacy & Safety Settings (#1306)

| Rationale | Demo |
|---|---|
| Implements a settings screen to control your Privacy & Safety settings
from within the App. | ![RocketSim_Recording_iPhone_15_6 1_2024-05-30_11
58
40](https://github.com/mastodon/mastodon-ios/assets/126418/90037659-f0f7-42c5-91ce-e0135f54c91a)
|
This commit is contained in:
Marcus Kida 2024-06-12 16:32:43 +02:00 committed by GitHub
commit dc07c65b9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 534 additions and 12 deletions

View File

@ -751,6 +751,7 @@
"title": "Settings",
"general": "General",
"notifications": "Notifications",
"privacy_safety": "Privacy & Safety",
"support_mastodon": "Support Mastodon",
"about_mastodon": "About Mastodon",
"server_details": "Server Details",
@ -821,6 +822,25 @@
"go_to_settings": "Go to Notification Settings"
}
},
"privacy_safety": {
"title": "Privacy & Safety",
"preset": {
"title": "Preset",
"open_and_public": "Open & Public",
"private_and_restricted": "Private & Restricted",
"custom": "Custom"
},
"default_post_visibility": {
"title": "Default Post Visibility",
"public": "Public",
"followers_only": "Followers Only",
"only_people_mentioned": "Only People Mentioned"
},
"manually_approve_follow_requests": "Manually Approve Follow Requests",
"show_followers_and_following": "Show Followers & Following",
"suggest_my_account_to_others": "Suggest My Account to Others",
"appear_in_search_engines": "Appear in Search Engines"
}
},
"report": {

View File

@ -21,6 +21,8 @@
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; };
2A0BF97F2C0622AA004A1E29 /* PrivacySafetyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0BF97E2C0622AA004A1E29 /* PrivacySafetyViewController.swift */; };
2A0BF9812C06252A004A1E29 /* PrivacySafetyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0BF9802C06252A004A1E29 /* PrivacySafetyViewModel.swift */; };
2A1BF99529F7E68400FA1BA5 /* DataSourceFacade+UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1BF99429F7E68400FA1BA5 /* DataSourceFacade+UserView.swift */; };
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; };
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
@ -32,6 +34,9 @@
2A409F832B5955290044E472 /* MastodonStatusThreadViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */; };
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */; };
2A5242732C199C96005B9E22 /* PrivacySafetyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5242722C199C96005B9E22 /* PrivacySafetyView.swift */; };
2A5242752C199CBD005B9E22 /* CheckableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5242742C199CBD005B9E22 /* CheckableButton.swift */; };
2A5242772C199EC2005B9E22 /* PrivacySafetySettingPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5242762C199EC2005B9E22 /* PrivacySafetySettingPreset.swift */; };
2A631AE82B8C9F6600FE0778 /* LanguagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */; };
2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */; };
2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -622,6 +627,8 @@
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; };
2A0BF97E2C0622AA004A1E29 /* PrivacySafetyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySafetyViewController.swift; sourceTree = "<group>"; };
2A0BF9802C06252A004A1E29 /* PrivacySafetyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySafetyViewModel.swift; sourceTree = "<group>"; };
2A1BF99429F7E68400FA1BA5 /* DataSourceFacade+UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+UserView.swift"; sourceTree = "<group>"; };
2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; };
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; };
@ -634,6 +641,9 @@
2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusThreadViewModel+State.swift"; sourceTree = "<group>"; };
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderView.swift; sourceTree = "<group>"; };
2A5242722C199C96005B9E22 /* PrivacySafetyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySafetyView.swift; sourceTree = "<group>"; };
2A5242742C199CBD005B9E22 /* CheckableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckableButton.swift; sourceTree = "<group>"; };
2A5242762C199EC2005B9E22 /* PrivacySafetySettingPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySafetySettingPreset.swift; sourceTree = "<group>"; };
2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerViewController.swift; sourceTree = "<group>"; };
2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1372,6 +1382,18 @@
path = CollectionViewCell;
sourceTree = "<group>";
};
2A0BF97D2C062278004A1E29 /* Privacy and Safety */ = {
isa = PBXGroup;
children = (
2A0BF97E2C0622AA004A1E29 /* PrivacySafetyViewController.swift */,
2A0BF9802C06252A004A1E29 /* PrivacySafetyViewModel.swift */,
2A5242722C199C96005B9E22 /* PrivacySafetyView.swift */,
2A5242742C199CBD005B9E22 /* CheckableButton.swift */,
2A5242762C199EC2005B9E22 /* PrivacySafetySettingPreset.swift */,
);
path = "Privacy and Safety";
sourceTree = "<group>";
};
2A506CF2292CD83B00059C37 /* FollowedTags */ = {
isa = PBXGroup;
children = (
@ -1686,6 +1708,7 @@
D8F916FF2A4AD898008A5370 /* Settings Overview */,
D8F917042A4B0657008A5370 /* General Settings */,
D81D12432A4E181C005009D4 /* Notification Settings */,
2A0BF97D2C062278004A1E29 /* Privacy and Safety */,
D80911062AC4BFD100EB4D15 /* Server Details */,
D8F917092A4B2AFF008A5370 /* About Mastodon */,
D8318A7F2A4466D300C0FB73 /* SettingsCoordinator.swift */,
@ -3437,6 +3460,7 @@
DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */,
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */,
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
2A5242752C199CBD005B9E22 /* CheckableButton.swift in Sources */,
DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */,
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */,
@ -3534,6 +3558,7 @@
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */,
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */,
2A0BF9812C06252A004A1E29 /* PrivacySafetyViewModel.swift in Sources */,
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */,
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
@ -3665,6 +3690,7 @@
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
DB7274F4273BB9B200577D95 /* UIScrollViewDelegate.swift in Sources */,
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */,
2A5242732C199C96005B9E22 /* PrivacySafetyView.swift in Sources */,
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
@ -3728,6 +3754,7 @@
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */,
D8318A862A4468C700C0FB73 /* SettingsViewController.swift in Sources */,
DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */,
2A0BF97F2C0622AA004A1E29 /* PrivacySafetyViewController.swift in Sources */,
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */,
@ -3755,6 +3782,7 @@
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */,
D8F917142A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift in Sources */,
2A5242772C199EC2005B9E22 /* PrivacySafetySettingPreset.swift in Sources */,
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,

View File

@ -0,0 +1,23 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import SwiftUI
import MastodonAsset
struct CheckableButton: View {
let text: String
let isChecked: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
Text(text)
Spacer()
if isChecked {
Image(systemName: "checkmark")
.foregroundStyle(Asset.Colors.Brand.blurple.swiftUIColor)
}
}
}
}
}

View File

@ -0,0 +1,60 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import Foundation
enum PrivacySafetySettingPreset: PrivacySafetySettingApplicable {
case openPublic, privateRestricted
var visibility: PrivacySafetyViewModel.Visibility {
switch self {
case .openPublic:
return .public
case .privateRestricted:
return .followersOnly
}
}
var manuallyApproveFollowRequests: Bool {
switch self {
case .openPublic:
return false
case .privateRestricted:
return true
}
}
var showFollowersAndFollowing: Bool {
switch self {
case .openPublic:
return true
case .privateRestricted:
return false
}
}
var suggestMyAccountToOthers: Bool {
switch self {
case .openPublic:
return true
case .privateRestricted:
return false
}
}
var appearInSearches: Bool {
switch self {
case .openPublic:
return true
case .privateRestricted:
return false
}
}
func equalsSettings(of viewModel: PrivacySafetyViewModel) -> Bool {
return viewModel.visibility == visibility &&
viewModel.manuallyApproveFollowRequests == manuallyApproveFollowRequests &&
viewModel.showFollowersAndFollowing == showFollowersAndFollowing &&
viewModel.suggestMyAccountToOthers == suggestMyAccountToOthers &&
viewModel.appearInSearches == appearInSearches
}
}

View File

@ -0,0 +1,65 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import SwiftUI
import MastodonLocalization
struct PrivacySafetyView: View {
@StateObject var viewModel: PrivacySafetyViewModel
var body: some View {
Group {
if !viewModel.isUserInteractionEnabled {
ProgressView()
} else {
Form {
Section(L10n.Scene.Settings.PrivacySafety.Preset.title) {
CheckableButton(
text: L10n.Scene.Settings.PrivacySafety.Preset.openAndPublic,
isChecked: viewModel.preset == .openPublic,
action: {
viewModel.preset = .openPublic
}
)
CheckableButton(
text: L10n.Scene.Settings.PrivacySafety.Preset.privateAndRestricted,
isChecked: viewModel.preset == .privateRestricted,
action: {
viewModel.preset = .privateRestricted
}
)
if viewModel.preset == .custom {
CheckableButton(
text: L10n.Scene.Settings.PrivacySafety.Preset.custom,
isChecked: viewModel.preset == .custom,
action: {
viewModel.preset = .custom
}
)
}
}
Section {
Picker(selection: $viewModel.visibility) {
ForEach(PrivacySafetyViewModel.Visibility.allCases, id: \.self) {
Text($0.title)
}
} label: {
Text(L10n.Scene.Settings.PrivacySafety.DefaultPostVisibility.title)
}
}
Section {
Toggle(L10n.Scene.Settings.PrivacySafety.manuallyApproveFollowRequests, isOn: $viewModel.manuallyApproveFollowRequests)
Toggle(L10n.Scene.Settings.PrivacySafety.showFollowersAndFollowing, isOn: $viewModel.showFollowersAndFollowing)
Toggle(L10n.Scene.Settings.PrivacySafety.suggestMyAccountToOthers, isOn: $viewModel.suggestMyAccountToOthers)
Toggle(L10n.Scene.Settings.PrivacySafety.appearInSearchEngines, isOn: $viewModel.appearInSearches)
}
}
}
}
.onAppear(perform: viewModel.viewDidAppear)
.onDisappear(perform: viewModel.saveSettings)
}
}

View File

@ -0,0 +1,39 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import Combine
import UIKit
import SwiftUI
import MastodonSDK
import MastodonCore
import MastodonLocalization
import MastodonAsset
final class PrivacySafetyViewController: UIHostingController<PrivacySafetyView> {
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) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = L10n.Scene.Settings.PrivacySafety.title
}
}

View File

@ -0,0 +1,205 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import Combine
import Foundation
import MastodonLocalization
import MastodonCore
import MastodonSDK
protocol PrivacySafetySettingApplicable {
var visibility: PrivacySafetyViewModel.Visibility { get }
var manuallyApproveFollowRequests: Bool { get }
var showFollowersAndFollowing: Bool { get }
var suggestMyAccountToOthers: Bool { get }
var appearInSearches: Bool { get }
}
class PrivacySafetyViewModel: ObservableObject, PrivacySafetySettingApplicable {
enum Preset {
case openPublic, privateRestricted, custom
}
enum Visibility: CaseIterable {
case `public`, followersOnly, onlyPeopleMentioned
var title: String {
switch self {
case .public:
return L10n.Scene.Settings.PrivacySafety.DefaultPostVisibility.public
case .followersOnly:
return L10n.Scene.Settings.PrivacySafety.DefaultPostVisibility.followersOnly
case .onlyPeopleMentioned:
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
}
}
}
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) }
}
@Published var visibility: Visibility = .public {
didSet { evaluatePreset() }
}
@Published var manuallyApproveFollowRequests = false {
didSet { evaluatePreset() }
}
@Published var showFollowersAndFollowing = true {
didSet { evaluatePreset() }
}
@Published var suggestMyAccountToOthers = true {
didSet { evaluatePreset() }
}
@Published var appearInSearches = true {
didSet { evaluatePreset() }
}
private var doNotEvaluate = true
@Published var isUserInteractionEnabled = false
let onDismiss = PassthroughSubject<Void, Never>()
func viewDidAppear() {
doNotEvaluate = false
if !isUserInteractionEnabled {
loadSettings()
}
}
}
extension PrivacySafetyViewModel: Equatable {
static func == (lhs: PrivacySafetyViewModel, rhs: PrivacySafetyViewModel) -> Bool {
lhs.visibility == rhs.visibility &&
lhs.manuallyApproveFollowRequests == rhs.manuallyApproveFollowRequests &&
lhs.showFollowersAndFollowing == rhs.showFollowersAndFollowing &&
lhs.suggestMyAccountToOthers == rhs.suggestMyAccountToOthers &&
lhs.appearInSearches == rhs.appearInSearches
}
}
extension PrivacySafetyViewModel {
func applyPreset(_ preset: Preset) {
switch preset {
case .openPublic:
self.apply(from: .openPublic)
case .privateRestricted:
self.apply(from: .privateRestricted)
case .custom:
break
}
}
func evaluatePreset() {
guard !doNotEvaluate else { return }
if PrivacySafetySettingPreset.openPublic.equalsSettings(of: self) {
preset = .openPublic
} else if PrivacySafetySettingPreset.privateRestricted.equalsSettings(of: self) {
preset = .privateRestricted
} else {
preset = .custom
}
}
private 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.source?.hideCollections == false
suggestMyAccountToOthers = account.source?.discoverable == true
appearInSearches = account.source?.indexable == true
isUserInteractionEnabled = 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
)
}
}
func dismiss() {
onDismiss.send(())
}
}
// Preset Rules Definition
extension PrivacySafetyViewModel {
private func apply(from source: PrivacySafetySettingPreset) {
doNotEvaluate = true
visibility = source.visibility
manuallyApproveFollowRequests = source.manuallyApproveFollowRequests
showFollowersAndFollowing = source.showFollowersAndFollowing
suggestMyAccountToOthers = source.suggestMyAccountToOthers
appearInSearches = source.appearInSearches
doNotEvaluate = false
}
}

View File

@ -10,6 +10,7 @@ struct SettingsSection: Hashable {
enum SettingsEntry: Hashable {
case general
case notifications
case privacySafety
case serverDetails(domain: String)
case aboutMastodon
case logout(accountName: String)
@ -20,6 +21,8 @@ enum SettingsEntry: Hashable {
return L10n.Scene.Settings.Overview.general
case .notifications:
return L10n.Scene.Settings.Overview.notifications
case .privacySafety:
return L10n.Scene.Settings.Overview.privacySafety
case .serverDetails(_):
return L10n.Scene.Settings.Overview.serverDetails
case .aboutMastodon:
@ -33,14 +36,14 @@ enum SettingsEntry: Hashable {
switch self {
case .serverDetails(domain: let domain):
return domain
case .general, .notifications, .aboutMastodon, .logout(_):
case .general, .notifications, .privacySafety, .aboutMastodon, .logout(_):
return nil
}
}
var accessoryType: UITableViewCell.AccessoryType {
switch self {
case .general, .notifications, .serverDetails(_), .aboutMastodon, .logout(_):
case .general, .notifications, .privacySafety, .serverDetails(_), .aboutMastodon, .logout(_):
return .disclosureIndicator
}
}
@ -51,6 +54,8 @@ enum SettingsEntry: Hashable {
return UIImage(systemName: "gear")
case .notifications:
return UIImage(systemName: "bell.badge")
case .privacySafety:
return UIImage(systemName: "lock.fill")
case .serverDetails(_):
return UIImage(systemName: "server.rack")
case .aboutMastodon:
@ -66,6 +71,8 @@ enum SettingsEntry: Hashable {
return .systemGray
case .notifications:
return .systemRed
case .privacySafety:
return .systemBlue
case .serverDetails(_):
return .systemTeal
case .aboutMastodon:
@ -78,7 +85,7 @@ enum SettingsEntry: Hashable {
var textColor: UIColor {
switch self {
case .general, .notifications, .aboutMastodon, .serverDetails(_):
case .general, .notifications, .privacySafety, .aboutMastodon, .serverDetails(_):
return .label
case .logout(_):
return .red

View File

@ -19,10 +19,10 @@ class SettingsViewController: UIViewController {
init(accountName: String, domain: String) {
sections = [
.init(entries: [.general, .notifications]),
.init(entries: [.serverDetails(domain: domain), .aboutMastodon]),
.init(entries: [.logout(accountName: accountName)])
]
.init(entries: [.general, .notifications, .privacySafety]),
.init(entries: [.serverDetails(domain: domain), .aboutMastodon]),
.init(entries: [.logout(accountName: accountName)])
]
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false

View File

@ -71,6 +71,13 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
notificationViewController.delegate = self
navigationController.pushViewController(notificationViewController, animated: true)
case .privacySafety:
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)
serverDetailsViewController.delegate = self

View File

@ -1598,6 +1598,8 @@ public enum L10n {
}
/// Notifications
public static let notifications = L10n.tr("Localizable", "Scene.Settings.Overview.Notifications", fallback: "Notifications")
/// Privacy & Safety
public static let privacySafety = L10n.tr("Localizable", "Scene.Settings.Overview.PrivacySafety", fallback: "Privacy & Safety")
/// Server Details
public static let serverDetails = L10n.tr("Localizable", "Scene.Settings.Overview.ServerDetails", fallback: "Server Details")
/// Support Mastodon
@ -1605,6 +1607,38 @@ public enum L10n {
/// Settings
public static let title = L10n.tr("Localizable", "Scene.Settings.Overview.Title", fallback: "Settings")
}
public enum PrivacySafety {
/// Appear in Search Engines
public static let appearInSearchEngines = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.AppearInSearchEngines", fallback: "Appear in Search Engines")
/// Manually Approve Follow Requests
public static let manuallyApproveFollowRequests = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.ManuallyApproveFollowRequests", fallback: "Manually Approve Follow Requests")
/// Show Followers & Following
public static let showFollowersAndFollowing = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.ShowFollowersAndFollowing", fallback: "Show Followers & Following")
/// Suggest My Account to Others
public static let suggestMyAccountToOthers = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.SuggestMyAccountToOthers", fallback: "Suggest My Account to Others")
/// Privacy & Safety
public static let title = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.Title", fallback: "Privacy & Safety")
public enum DefaultPostVisibility {
/// Followers Only
public static let followersOnly = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.DefaultPostVisibility.FollowersOnly", fallback: "Followers Only")
/// Only People Mentioned
public static let onlyPeopleMentioned = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.DefaultPostVisibility.OnlyPeopleMentioned", fallback: "Only People Mentioned")
/// Public
public static let `public` = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.DefaultPostVisibility.Public", fallback: "Public")
/// Default Post Visibility
public static let title = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.DefaultPostVisibility.Title", fallback: "Default Post Visibility")
}
public enum Preset {
/// Custom
public static let custom = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.Preset.Custom", fallback: "Custom")
/// Open & Public
public static let openAndPublic = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.Preset.OpenAndPublic", fallback: "Open & Public")
/// Private & Restricted
public static let privateAndRestricted = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.Preset.PrivateAndRestricted", fallback: "Private & Restricted")
/// Preset
public static let title = L10n.tr("Localizable", "Scene.Settings.PrivacySafety.Preset.Title", fallback: "Preset")
}
}
public enum ServerDetails {
/// About
public static let about = L10n.tr("Localizable", "Scene.Settings.ServerDetails.About", fallback: "About")

View File

@ -558,9 +558,23 @@ If you disagree with the policy for **%@**, you can go back and pick a different
"Scene.Settings.Overview.General" = "General";
"Scene.Settings.Overview.Logout" = "Logout %@";
"Scene.Settings.Overview.Notifications" = "Notifications";
"Scene.Settings.Overview.PrivacySafety" = "Privacy & Safety";
"Scene.Settings.Overview.ServerDetails" = "Server Details";
"Scene.Settings.Overview.SupportMastodon" = "Support Mastodon";
"Scene.Settings.Overview.Title" = "Settings";
"Scene.Settings.PrivacySafety.Title" = "Privacy & Safety";
"Scene.Settings.PrivacySafety.Preset.Title" = "Preset";
"Scene.Settings.PrivacySafety.Preset.OpenAndPublic" = "Open & Public";
"Scene.Settings.PrivacySafety.Preset.PrivateAndRestricted" = "Private & Restricted";
"Scene.Settings.PrivacySafety.Preset.Custom" = "Custom";
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Title" = "Default Post Visibility";
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Public" = "Public";
"Scene.Settings.PrivacySafety.DefaultPostVisibility.FollowersOnly" = "Followers Only";
"Scene.Settings.PrivacySafety.DefaultPostVisibility.OnlyPeopleMentioned" = "Only People Mentioned";
"Scene.Settings.PrivacySafety.ManuallyApproveFollowRequests" = "Manually Approve Follow Requests";
"Scene.Settings.PrivacySafety.ShowFollowersAndFollowing" = "Show Followers & Following";
"Scene.Settings.PrivacySafety.SuggestMyAccountToOthers" = "Suggest My Account to Others";
"Scene.Settings.PrivacySafety.AppearInSearchEngines" = "Appear in Search Engines";
"Scene.Settings.ServerDetails.About" = "About";
"Scene.Settings.ServerDetails.AboutInstance.LegalNotice" = "A legal notice";
"Scene.Settings.ServerDetails.AboutInstance.MessageAdmin" = "Message Admin";
@ -601,4 +615,4 @@ If you disagree with the policy for **%@**, you can go back and pick a different
"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

@ -158,7 +158,9 @@ extension Mastodon.API.Account {
public let locked: Bool?
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
@ -170,6 +172,8 @@ extension Mastodon.API.Account {
case locked
case source
case fieldsAttributes = "fields_attributes"
case indexable
case hideCollections = "hide_collections"
}
public init(
@ -181,7 +185,9 @@ extension Mastodon.API.Account {
header: Mastodon.Query.MediaAttachment? = nil,
locked: Bool? = nil,
source: Mastodon.Entity.Source? = nil,
fieldsAttributes: [Mastodon.Entity.Field]? = nil
fieldsAttributes: [Mastodon.Entity.Field]? = nil,
indexable: Bool? = nil,
hideCollections: Bool? = nil
) {
self.discoverable = discoverable
self.bot = bot
@ -192,6 +198,8 @@ extension Mastodon.API.Account {
self.locked = locked
self.source = source
self.fieldsAttributes = fieldsAttributes
self.indexable = indexable
self.hideCollections = hideCollections
}
var contentType: String? {
@ -205,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)) }
@ -212,6 +221,7 @@ extension Mastodon.API.Account {
avatar.flatMap { data.append(Data.multipart(key: "avatar", value: $0)) }
header.flatMap { data.append(Data.multipart(key: "header", value: $0)) }
locked.flatMap { data.append(Data.multipart(key: "locked", value: $0)) }
indexable.flatMap { data.append(Data.multipart(key: "indexable", value: $0)) }
if let source = source {
source.privacy.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0.rawValue)) }
source.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) }

View File

@ -71,7 +71,7 @@ extension Mastodon.Entity.Account: Codable {
case locked
case emojis
case discoverable
case createdAt = "created_at"
case lastStatusAt = "last_status_at"
case statusesCount = "statuses_count"

View File

@ -26,7 +26,10 @@ extension Mastodon.Entity {
public let sensitive: Bool?
public let language: String? // (ISO 639-1 language two-letter code)
public let followRequestsCount: Int?
public let hideCollections: Bool?
public let indexable: Bool?
public let discoverable: Bool?
enum CodingKeys: String, CodingKey {
case note
case fields
@ -35,6 +38,13 @@ extension Mastodon.Entity {
case sensitive
case language
case followRequestsCount = "follow_requests_count"
case hideCollections = "hide_collections"
case indexable
case discoverable
}
public static func withPrivacy(_ privacy: Privacy) -> Self {
Source(note: "", fields: nil, privacy: privacy, sensitive: nil, language: nil, followRequestsCount: nil, hideCollections: nil, indexable: nil, discoverable: nil)
}
}
}