diff --git a/Localization/app.json b/Localization/app.json index aad9d3ad7..812a801ac 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -172,6 +172,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" } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2cfaa4acf..9e86ad664 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 */; }; @@ -437,6 +439,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 = ""; }; + 5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = ""; }; 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; @@ -1006,6 +1010,15 @@ name = Frameworks; sourceTree = ""; }; + 5D03938E2612D200007FE196 /* Webview */ = { + isa = PBXGroup; + children = ( + 5D03938F2612D259007FE196 /* WebViewController.swift */, + 5D0393952612D266007FE196 /* WebViewModel.swift */, + ); + path = Webview; + sourceTree = ""; + }; DB01409B25C40BB600F9F3CF /* Onboarding */ = { isa = PBXGroup; children = ( @@ -1387,6 +1400,7 @@ DB8AF55525C1379F002E6C99 /* Scene */ = { isa = PBXGroup; children = ( + 5D03938E2612D200007FE196 /* Webview */, 2D7631A425C1532200929FB9 /* Share */, DB8AF54E25C13703002E6C99 /* MainTab */, DB01409B25C40BB600F9F3CF /* Onboarding */, @@ -1995,6 +2009,7 @@ DBB525502611ED6D002F1F29 /* ProfileHeaderView.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 */, @@ -2082,6 +2097,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 */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 0f343c855..d578ee528 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -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) @@ -200,6 +201,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 diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index a0c8c8e05..a308033fc 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -446,6 +446,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)) @@ -454,6 +456,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 { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 252335a1c..88ecd2508 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -143,8 +143,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."; \ No newline at end of file diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index e0c6e3605..5fb526218 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -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 = { @@ -90,36 +93,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 @@ -169,8 +175,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 } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 89f31bbc2..14b4f0941 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -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 diff --git a/Mastodon/Scene/Webview/WebViewController.swift b/Mastodon/Scene/Webview/WebViewController.swift new file mode 100644 index 000000000..bde6e8936 --- /dev/null +++ b/Mastodon/Scene/Webview/WebViewController.swift @@ -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() + 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) + } +} diff --git a/Mastodon/Scene/Webview/WebViewModel.swift b/Mastodon/Scene/Webview/WebViewModel.swift new file mode 100644 index 000000000..4e6483c98 --- /dev/null +++ b/Mastodon/Scene/Webview/WebViewModel.swift @@ -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 +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index ac960e710..376dbeb36 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -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 {