Adjust Server Rules Screen (IOS-220) (#1251)

* Adjust Server Rules Screen (IOS-220)

* Update Server Rules (IOS-220)

* Use new server rules UI in Server Details (IOS-220)

* Improve disclaimer usage (IOS-220)

* Fix background in server details (IOS-220)
This commit is contained in:
Marcus Kida 2024-03-19 09:55:42 +01:00 committed by GitHub
parent 62cc9105a9
commit 484d72fbdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 177 additions and 137 deletions

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -416,8 +416,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "գաղտնիութեան քաղաքականութիւն",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "zasady użytkowania",
"privacy_policy": "polityka prywatności",

View File

@ -418,7 +418,7 @@
},
"server_rules": {
"title": "Algumas regras básicas.",
"subtitle": "These are set and enforced by the %s moderators.",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",

View File

@ -417,8 +417,8 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "සේවාවේ නියම",
"privacy_policy": "රහස්‍යතා ප්‍රතිපත්තිය",

View File

@ -435,13 +435,14 @@
}
},
"server_rules": {
"title": "Some ground rules.",
"subtitle": "These are set and enforced by the %s moderators.",
"title": "Server Rules",
"subtitle": "By continuing, you agree to follow by the following rules set and enforced by the **%s** moderators.",
"prompt": "By continuing, youre subject to the terms of service and privacy policy for %s.",
"terms_of_service": "terms of service",
"privacy_policy": "privacy policy",
"button": {
"confirm": "I Agree"
"confirm": "I Agree",
"disagree": "Disagree"
}
},
"confirm_email": {

View File

@ -409,8 +409,7 @@ private extension SceneCoordinator {
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonServerRules(let viewModel):
let _viewController = MastodonServerRulesViewController()
_viewController.viewModel = viewModel
let _viewController = MastodonServerRulesViewController(viewModel: viewModel)
viewController = _viewController
case .mastodonConfirmEmail(let viewModel):
let _viewController = MastodonConfirmEmailViewController()

View File

@ -13,22 +13,121 @@ import MetaTextKit
import MastodonAsset
import MastodonCore
import MastodonLocalization
import SwiftUI
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
struct MastodonServerRulesView: View {
class ViewModel: ObservableObject {
let disclaimer: LocalizedStringKey?
let rules: [String]
var onAgree: (() -> Void)?
var onDisagree: (() -> Void)?
init(disclaimer: LocalizedStringKey?, rules: [String], onAgree: (() -> Void)?, onDisagree: (() -> Void)?) {
self.disclaimer = disclaimer
self.rules = rules
self.onAgree = onAgree
self.onDisagree = onDisagree
}
fileprivate static var empty: ViewModel {
return .init(disclaimer: nil, rules: [], onAgree: nil, onDisagree: nil)
}
}
@ObservedObject var viewModel: ViewModel = .empty
var body: some View {
ScrollView {
VStack(alignment: .leading) {
if let disclaimer = viewModel.disclaimer {
Text(disclaimer)
.padding(.bottom, 30)
}
ForEach(Array(viewModel.rules.enumerated()), id: \.offset) { index, rule in
ZStack(alignment: .topLeading) {
Text("\(index + 1)")
.font(.system(size: UIFontMetrics.default.scaledValue(for: 24), weight: .bold))
.foregroundStyle(Asset.Colors.Brand.blurple.swiftUIColor)
Text(rule)
.padding(.leading, 30)
}
.padding(.bottom, 30)
}
}
}
.padding(.horizontal)
.safeAreaInset(edge: .bottom) {
if viewModel.onDisagree != nil || viewModel.onAgree != nil {
VStack {
if let onDisagree = viewModel.onDisagree {
Button(role: .cancel) {
onDisagree()
} label: {
Text(L10n.Scene.ServerRules.Button.disagree)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.clear)
.foregroundStyle(Asset.Colors.Brand.blurple.swiftUIColor)
}
if let onAgree = viewModel.onAgree {
Button {
onAgree()
} label: {
Text(L10n.Scene.ServerRules.Button.confirm)
.frame(maxWidth: .infinity)
.bold()
}
.buttonStyle(.borderedProminent)
}
}
.controlSize(.large)
.padding()
.background(.ultraThinMaterial)
.tint(Asset.Colors.Brand.blurple.swiftUIColor)
}
}
}
}
private struct MastodonServerRulesButton: View {
let text: LocalizedStringKey
let action: () -> Void
var body: some View {
Button(action: action) {
Text(text)
.frame(height: 44)
.frame(maxWidth: .infinity)
}
.font(Font(UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16, weight: .semibold))))
}
}
final class MastodonServerRulesViewController: UIHostingController<MastodonServerRulesView>, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: MastodonServerRulesViewModel!
let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.register(ServerRulesTableViewCell.self, forCellReuseIdentifier: String(describing: ServerRulesTableViewCell.self))
tableView.rowHeight = UITableView.automaticDimension
tableView.keyboardDismissMode = .onDrag
tableView.sectionHeaderTopPadding = 0
return tableView
}()
private var viewModel: MastodonServerRulesViewModel!
init(viewModel: MastodonServerRulesViewModel) {
super.init(rootView: MastodonServerRulesView())
self.viewModel = viewModel
self.rootView.viewModel = .init(
disclaimer: LocalizedStringKey(L10n.Scene.ServerRules.subtitle(viewModel.domain)),
rules: viewModel.rules.map({ $0.text }),
onAgree: { self.nextButtonPressed(nil) },
onDisagree: { self.backButtonPressed(nil) })
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension MastodonServerRulesViewController {
@ -38,34 +137,18 @@ extension MastodonServerRulesViewController {
setupOnboardingAppearance()
defer { setupNavigationBarBackgroundView() }
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.pinToParent()
tableView.delegate = self
viewModel.setupDiffableDataSource(tableView: tableView)
navigationItem.largeTitleDisplayMode = .always
navigationItem.rightBarButtonItem = UIBarButtonItem(title: L10n.Scene.ServerRules.Button.confirm, style: .done, target: self, action: #selector(MastodonServerRulesViewController.nextButtonPressed(_:)))
title = L10n.Scene.ServerRules.title
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.flashScrollIndicators()
}
}
extension MastodonServerRulesViewController {
@objc private func backButtonPressed(_ sender: UIButton) {
@objc private func backButtonPressed(_ sender: UIButton?) {
navigationController?.popViewController(animated: true)
}
@objc private func nextButtonPressed(_ sender: UIButton) {
@objc private func nextButtonPressed(_ sender: UIButton?) {
let domain = viewModel.domain
let viewModel = PrivacyViewModel(domain: domain, authenticateInfo: viewModel.authenticateInfo, rows: [.iOSApp, .server(domain: domain)], instance: viewModel.instance, applicationToken: viewModel.applicationToken)
@ -75,28 +158,3 @@ extension MastodonServerRulesViewController {
// MARK: - OnboardingViewControllerAppearance
extension MastodonServerRulesViewController: OnboardingViewControllerAppearance { }
// MARK: - UITableViewDelegate
extension MastodonServerRulesViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let wrapper = UIView()
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = MastodonPickServerViewController.subTitleFont
label.textColor = Asset.Colors.Label.primary.color
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0
label.text = L10n.Scene.ServerRules.subtitle(viewModel.domain)
wrapper.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 16),
label.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
wrapper.trailingAnchor.constraint(equalTo: label.trailingAnchor),
wrapper.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 16),
])
return wrapper
}
}

View File

@ -2,57 +2,37 @@
import UIKit
import MastodonSDK
import SwiftUI
protocol InstanceRulesViewControllerDelegate: AnyObject {
}
class InstanceRulesViewController: UIViewController {
struct InstanceRulesView: View {
var rulesView = MastodonServerRulesView()
var body: some View {
rulesView
.padding([.top], 16)
}
}
weak var delegate: InstanceRulesViewControllerDelegate?
let tableView: UITableView
var dataSource: UITableViewDiffableDataSource<ServerRuleSection, ServerRuleItem>?
var sections: [ServerRuleSection] = []
class InstanceRulesViewController: UIHostingController<InstanceRulesView> {
init() {
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(ServerRulesTableViewCell.self, forCellReuseIdentifier: ServerRulesTableViewCell.reuseIdentifier)
super.init(nibName: nil, bundle: nil)
view.addSubview(tableView)
let dataSource = ServerRuleSection.tableViewDiffableDataSource(tableView: tableView)
tableView.dataSource = dataSource
self.dataSource = dataSource
setupConstraints()
super.init(rootView: InstanceRulesView())
view.backgroundColor = .systemGroupedBackground
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupConstraints() {
let constraints = [
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor),
]
NSLayoutConstraint.activate(constraints)
}
func update(with instance: Mastodon.Entity.V2.Instance) {
guard let dataSource, let rules = instance.rules, rules.isNotEmpty else { return }
var snapshot = NSDiffableDataSourceSnapshot<ServerRuleSection, ServerRuleItem>()
snapshot.appendSections([.rules])
let ruleItems = rules.enumerated().compactMap { index, rule in ServerRuleItem.rule(index: index, rule: rule) }
snapshot.appendItems(ruleItems, toSection: .rules)
dataSource.apply(snapshot)
self.rootView.rulesView.viewModel = .init(
disclaimer: nil,
rules: instance.rules?.map({ $0.text }) ?? [],
onAgree: nil,
onDisagree: nil
)
}
}

View File

@ -25,7 +25,6 @@ class ServerDetailsViewController: UIViewController {
weak var delegate: (ServerDetailsViewControllerDelegate & AboutInstanceViewControllerDelegate & InstanceRulesViewControllerDelegate & MetaLabelDelegate)? {
didSet {
aboutInstanceViewController.delegate = delegate
instanceRulesViewController.delegate = delegate
aboutInstanceViewController.footerView.contentLabel.linkDelegate = delegate
}
}

View File

@ -1455,17 +1455,19 @@ public enum L10n {
public static func prompt(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1), fallback: "By continuing, youre subject to the terms of service and privacy policy for %@.")
}
/// These are set and enforced by the %@ moderators.
/// By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.
public static func subtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1), fallback: "These are set and enforced by the %@ moderators.")
return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1), fallback: "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.")
}
/// terms of service
public static let termsOfService = L10n.tr("Localizable", "Scene.ServerRules.TermsOfService", fallback: "terms of service")
/// Some ground rules.
public static let title = L10n.tr("Localizable", "Scene.ServerRules.Title", fallback: "Some ground rules.")
/// Server Rules
public static let title = L10n.tr("Localizable", "Scene.ServerRules.Title", fallback: "Server Rules")
public enum Button {
/// I Agree
public static let confirm = L10n.tr("Localizable", "Scene.ServerRules.Button.Confirm", fallback: "I Agree")
/// Disagree
public static let disagree = L10n.tr("Localizable", "Scene.ServerRules.Button.Disagree", fallback: "Disagree")
}
}
public enum Settings {

View File

@ -501,11 +501,12 @@ uploaded to Mastodon.";
"Scene.ServerPicker.SignupSpeed.ManuallyReviewed" = "Manual Review";
"Scene.ServerPicker.Title" = "Pick Server";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.Button.Disagree" = "Disagree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators.";
"Scene.ServerRules.Subtitle" = "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.ServerRules.Title" = "Server Rules";
"Scene.Settings.AboutMastodon.ClearMediaStorage" = "Clear Media Storage";
"Scene.Settings.AboutMastodon.ContributeToMastodon" = "Contribute to Mastodon";
"Scene.Settings.AboutMastodon.MoreSettings" = "Even More Settings";

View File

@ -491,9 +491,9 @@ uploaded to Mastodon.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators.";
"Scene.ServerRules.Subtitle" = "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.ServerRules.Title" = "Server Rules";
"Scene.Settings.AboutMastodon.ClearMediaStorage" = "Clear Media Storage";
"Scene.Settings.AboutMastodon.ContributeToMastodon" = "Contribute to Mastodon";
"Scene.Settings.AboutMastodon.MoreSettings" = "Even More Settings";
@ -568,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

@ -486,9 +486,9 @@ uploaded to Mastodon.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators.";
"Scene.ServerRules.Subtitle" = "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.ServerRules.Title" = "Server Rules";
"Scene.Settings.AboutMastodon.ClearMediaStorage" = "Clear Media Storage";
"Scene.Settings.AboutMastodon.ContributeToMastodon" = "Contribute to Mastodon";
"Scene.Settings.AboutMastodon.MoreSettings" = "Even More Settings";
@ -563,4 +563,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

@ -491,9 +491,9 @@ uploaded to Mastodon.";
"Scene.ServerRules.Button.Confirm" = "Համաձայն եմ";
"Scene.ServerRules.PrivacyPolicy" = "գաղտնիութեան քաղաքականութիւն";
"Scene.ServerRules.Prompt" = "By continuing, youre subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators.";
"Scene.ServerRules.Subtitle" = "By continuing, you agree to follow by the following rules set and enforced by the **%@** moderators.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.ServerRules.Title" = "Server Rules";
"Scene.Settings.AboutMastodon.ClearMediaStorage" = "Clear Media Storage";
"Scene.Settings.AboutMastodon.ContributeToMastodon" = "Contribute to Mastodon";
"Scene.Settings.AboutMastodon.MoreSettings" = "Even More Settings";
@ -568,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";