Merge pull request #80 from tootsuite/fix/serverRulesUI

fix: Update server rules scene UI design
This commit is contained in:
sxiaojian88 2021-04-01 16:00:13 +08:00 committed by GitHub
commit fa14477f7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 33 deletions

View File

@ -162,6 +162,8 @@
"title": "Some ground rules.",
"subtitle": "These rules are set by the admins of %s.",
"prompt": "By continuing, you're 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"
}

View File

@ -101,6 +101,8 @@
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; };
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
@ -410,6 +412,8 @@
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
@ -951,6 +955,15 @@
name = Frameworks;
sourceTree = "<group>";
};
5D03938E2612D200007FE196 /* Webview */ = {
isa = PBXGroup;
children = (
5D03938F2612D259007FE196 /* WebViewController.swift */,
5D0393952612D266007FE196 /* WebViewModel.swift */,
);
path = Webview;
sourceTree = "<group>";
};
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
isa = PBXGroup;
children = (
@ -1328,6 +1341,7 @@
DB8AF55525C1379F002E6C99 /* Scene */ = {
isa = PBXGroup;
children = (
5D03938E2612D200007FE196 /* Webview */,
2D7631A425C1532200929FB9 /* Share */,
DB8AF54E25C13703002E6C99 /* MainTab */,
DB01409B25C40BB600F9F3CF /* Onboarding */,
@ -1860,6 +1874,7 @@
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
@ -1939,6 +1954,7 @@
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,

View File

@ -46,6 +46,7 @@ extension SceneCoordinator {
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
case mastodonWebView(viewModel:WebViewModel)
// compose
case compose(viewModel: ComposeViewModel)
@ -193,6 +194,10 @@ private extension SceneCoordinator {
let _viewController = MastodonResendEmailViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonWebView(let viewModel):
let _viewController = WebViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .compose(let viewModel):
let _viewController = ComposeViewController()
_viewController.viewModel = viewModel

View File

@ -410,6 +410,8 @@ internal enum L10n {
}
}
internal enum ServerRules {
/// privacy policy
internal static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy")
/// By continuing, you're subject to the terms of service and privacy policy for %@.
internal static func prompt(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1))
@ -418,6 +420,8 @@ internal enum L10n {
internal static func subtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.ServerRules.Subtitle", String(describing: p1))
}
/// terms of service
internal static let termsOfService = L10n.tr("Localizable", "Scene.ServerRules.TermsOfService")
/// Some ground rules.
internal static let title = L10n.tr("Localizable", "Scene.ServerRules.Title")
internal enum Button {

View File

@ -129,8 +129,10 @@ tap the link to confirm your account.";
"Scene.ServerPicker.Title" = "Pick a Server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.Welcome.Slogan" = "Social networking
back in your hands.";

View File

@ -8,6 +8,8 @@
import os.log
import UIKit
import Combine
import MastodonSDK
import SafariServices
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
@ -44,19 +46,20 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
return label
}()
let bottonContainerView: UIView = {
let bottomContainerView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.Background.onboardingBackground.color
return view
}()
private(set) lazy var bottomPromptLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .label
label.text = L10n.Scene.ServerRules.prompt(viewModel.domain)
label.numberOfLines = 0
return label
private(set) lazy var bottomPromptTextView: UITextView = {
let textView = UITextView()
textView.font = .preferredFont(forTextStyle: .body)
textView.textColor = .label
textView.isSelectable = true
textView.isEditable = false
textView.backgroundColor = Asset.Colors.Background.onboardingBackground.color
return textView
}()
let confirmButton: PrimaryActionButton = {
@ -86,36 +89,39 @@ extension MastodonServerRulesViewController {
super.viewDidLoad()
setupOnboardingAppearance()
configTextView()
defer { setupNavigationBarBackgroundView() }
bottonContainerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottonContainerView)
bottomContainerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomContainerView)
NSLayoutConstraint.activate([
view.bottomAnchor.constraint(equalTo: bottonContainerView.bottomAnchor),
bottonContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bottonContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: bottomContainerView.bottomAnchor),
bottomContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bottomContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
bottonContainerView.preservesSuperviewLayoutMargins = true
bottomContainerView.preservesSuperviewLayoutMargins = true
defer {
view.bringSubviewToFront(bottonContainerView)
view.bringSubviewToFront(bottomContainerView)
}
confirmButton.translatesAutoresizingMaskIntoConstraints = false
bottonContainerView.addSubview(confirmButton)
bottomContainerView.addSubview(confirmButton)
NSLayoutConstraint.activate([
bottonContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight),
confirmButton.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
bottonContainerView.readableContentGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
bottomContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight),
confirmButton.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
bottomContainerView.readableContentGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin),
confirmButton.heightAnchor.constraint(equalToConstant: MastodonServerRulesViewController.actionButtonHeight).priority(.defaultHigh),
])
bottomPromptLabel.translatesAutoresizingMaskIntoConstraints = false
bottonContainerView.addSubview(bottomPromptLabel)
bottomPromptTextView.translatesAutoresizingMaskIntoConstraints = false
bottomContainerView.addSubview(bottomPromptTextView)
NSLayoutConstraint.activate([
bottomPromptLabel.topAnchor.constraint(equalTo: bottonContainerView.topAnchor, constant: 20),
bottomPromptLabel.leadingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.leadingAnchor),
bottomPromptLabel.trailingAnchor.constraint(equalTo: bottonContainerView.readableContentGuide.trailingAnchor),
confirmButton.topAnchor.constraint(equalTo: bottomPromptLabel.bottomAnchor, constant: 20),
bottomPromptTextView.frameLayoutGuide.topAnchor.constraint(equalTo: bottomContainerView.topAnchor, constant: 20),
bottomPromptTextView.frameLayoutGuide.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor),
bottomPromptTextView.frameLayoutGuide.trailingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.trailingAnchor),
bottomPromptTextView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 50),
confirmButton.topAnchor.constraint(equalTo: bottomPromptTextView.frameLayoutGuide.bottomAnchor, constant: 20),
])
scrollView.translatesAutoresizingMaskIntoConstraints = false
@ -165,8 +171,32 @@ extension MastodonServerRulesViewController {
extension MastodonServerRulesViewController {
func updateScrollViewContentInset() {
view.layoutIfNeeded()
scrollView.contentInset.bottom = bottonContainerView.frame.height
scrollView.verticalScrollIndicatorInsets.bottom = bottonContainerView.frame.height
scrollView.contentInset.bottom = bottomContainerView.frame.height
scrollView.verticalScrollIndicatorInsets.bottom = bottomContainerView.frame.height
}
func configTextView() {
let linkColor = Asset.Colors.Button.normal.color
let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain))
let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService)
let privacyRange = str.range(of: L10n.Scene.ServerRules.privacyPolicy)
let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body), NSAttributedString.Key.foregroundColor: UIColor.label])
attributeString.addAttribute(.link, value: Mastodon.API.serverRulesURL(domain: viewModel.domain), range: termsOfServiceRange)
attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange)
let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor]
bottomPromptTextView.attributedText = attributeString
bottomPromptTextView.linkTextAttributes = linkAttributes
bottomPromptTextView.delegate = self
}
}
extension MastodonServerRulesViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
let safariVC = SFSafariViewController(url: URL)
self.present(safariVC, animated: true, completion: nil)
return false
}
}

View File

@ -35,13 +35,16 @@ final class MastodonServerRulesViewModel {
var rulesAttributedString: NSAttributedString {
let attributedString = NSMutableAttributedString(string: "\n")
let configuration = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title3))
for (i, rule) in rules.enumerated() {
let index = String(i + 1)
let indexString = NSAttributedString(string: index + ". ", attributes: [
NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel
])
let ruleString = NSAttributedString(string: rule.text + "\n\n")
attributedString.append(indexString)
let imageName = String(i + 1) + ".circle.fill"
let image = UIImage(systemName: imageName, withConfiguration: configuration)!
let attachment = NSTextAttachment()
attachment.image = image.withTintColor(.black)
let imageAttribute = NSAttributedString(attachment: attachment)
let ruleString = NSAttributedString(string: " " + rule.text + "\n\n")
attributedString.append(imageAttribute)
attributedString.append(ruleString)
}
return attributedString

View File

@ -0,0 +1,67 @@
//
// WebViewController.swift
// Mastodon
//
// Created by xiaojian sun on 2021/3/30.
//
import Foundation
import Combine
import os.log
import UIKit
import WebKit
final class WebViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: WebViewModel!
let webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
let webView = WKWebView(frame: .zero, configuration: configuration)
return webView
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
// cleanup cookie
let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore
httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
httpCookieStore.delete(cookie, completionHandler: nil)
}
}
}
}
extension WebViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(WebViewController.cancelBarButtonItemPressed(_:)))
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let request = URLRequest(url: viewModel.url)
webView.load(request)
}
}
extension WebViewController {
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
}

View File

@ -0,0 +1,17 @@
//
// WebViewModel.swift
// Mastodon
//
// Created by xiaojian sun on 2021/3/30.
//
import Foundation
final class WebViewModel {
public init(url: URL) {
self.url = url
}
// input
let url: URL
}

View File

@ -89,6 +89,13 @@ extension Mastodon.API {
return URL(string: "https://" + domain + "/auth/confirmation/new")!
}
public static func serverRulesURL(domain: String) -> URL {
return URL(string: "https://" + domain + "/about/more")!
}
public static func privacyURL(domain: String) -> URL {
return URL(string: "https://" + domain + "/terms")!
}
}
extension Mastodon.API {