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. |  |
This commit is contained in:
commit
dc07c65b9e
@ -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": {
|
||||
|
@ -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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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";
|
||||
|
@ -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)) }
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user