Signal-iOS/Signal/src/ViewControllers/SafetyTipsViewController.swift

466 lines
16 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalUI
import SignalServiceKit
public enum SafetyTipsType {
case contact
case group
}
public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
let contentScrollView = UIScrollView()
let stackView = UIStackView()
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
private enum Constants {
static let stackSpacing: CGFloat = 12.0
static let outerSpacing: CGFloat = 20.0
static let outerMargins: UIEdgeInsets = .init(hMargin: 24.0, vMargin: 0.0)
static let footerSpacing: CGFloat = 16.0
static let footerMargins: UIEdgeInsets = .init(
top: 0.0,
left: 24.0,
bottom: 42.0,
right: 24.0
)
static let buttonInsets: UIEdgeInsets = .init(
top: 16.0,
leading: 36.0,
bottom: 12.0,
trailing: 36.0
)
static let buttonEdgeInsets: UIEdgeInsets = .init(
hMargin: 0.0,
vMargin: 14.0
)
}
fileprivate enum SafetyTips: CaseIterable {
case crypto
case vagueMessages
case messagesWithLinks
case fakeBusiness
var image: UIImage? {
switch self {
case .crypto:
return UIImage(named: "safety-tip-1")
case .vagueMessages:
return UIImage(named: "safety-tip-2")
case .messagesWithLinks:
return UIImage(named: "safety-tip-3")
case .fakeBusiness:
return UIImage(named: "safety-tip-4")
}
}
var title: String {
switch self {
case .crypto:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_TITLE",
comment: "Message title describing the crypto safety tip."
)
case .vagueMessages:
return OWSLocalizedString(
"SAFETY_TIPS_VAGUE_MESSAGE_TITLE",
comment: "Message title describing the safety tip about vague messages."
)
case .messagesWithLinks:
return OWSLocalizedString(
"SAFETY_TIPS_MESSAGE_LINKS_TITLE",
comment: "Message title describing the safety tip about unknown links in messages."
)
case .fakeBusiness:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_BUSINESS_TITLE",
comment: "Message title describing the safety tip about unknown or fake businesses."
)
}
}
var body: String {
switch self {
case .crypto:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_BODY",
comment: "Message contents for the crypto safety tip."
)
case .vagueMessages:
return OWSLocalizedString(
"SAFETY_TIPS_VAGUE_MESSAGE_BODY",
comment: "Message contents for the vague message safety tip."
)
case .messagesWithLinks:
return OWSLocalizedString(
"SAFETY_TIPS_MESSAGE_LINKS_BODY",
comment: "Message contents for the unknown links in messages safety tip."
)
case .fakeBusiness:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_BUSINESS_BODY",
comment: "Message contents for the safety tip concerning fake businesses."
)
}
}
}
public var prefersNavigationBarHidden: Bool { true }
private let type: SafetyTipsType
init(type: SafetyTipsType) {
self.type = type
super.init()
}
public override func viewDidLoad() {
minimizedHeight = min(725, CurrentAppContext().frame.height)
super.allowsExpansion = false
contentView.addSubview(contentScrollView)
contentScrollView.addSubview(stackView)
stackView.axis = .vertical
stackView.spacing = Constants.outerSpacing
stackView.isLayoutMarginsRelativeArrangement = true
stackView.autoPinEdgesToSuperviewEdges()
stackView.autoPinWidth(toWidthOf: contentScrollView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: 0.0, relation: .greaterThanOrEqual)
contentScrollView.translatesAutoresizingMaskIntoConstraints = false
contentScrollView.autoPinEdgesToSuperviewEdges()
buildContents()
updateButtonState()
setColorsForCurrentTheme()
super.viewDidLoad()
}
public override func themeDidChange() {
super.themeDidChange()
buildContents()
updateButtonState()
setColorsForCurrentTheme()
}
// MARK: - Views
private lazy var tipScrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
return scrollView
}()
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.numberOfPages = SafetyTips.allCases.count
pageControl.currentPage = 0
pageControl.addTarget(self, action: #selector(self.changePage), for: .valueChanged)
return pageControl
}()
private lazy var previousTipButton: OWSFlatButton = {
let previousButton = OWSFlatButton.insetButton(
title: OWSLocalizedString(
"SAFETY_TIPS_PREVIOUS_TIP_BUTTON",
comment: "Button that will show the previous safety tip."
),
font: .dynamicTypeBodyClamped.semibold(),
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: self,
selector: #selector(didTapPrevious))
previousButton.button.setBackgroundImage(UIImage.image(color: .clear), for: .disabled)
previousButton.button.setTitleColor(.ows_accentBlue, for: .disabled)
previousButton.contentEdgeInsets = Constants.buttonEdgeInsets
return previousButton
}()
private lazy var nextTipButton: OWSFlatButton = {
let nextButton = OWSFlatButton.insetButton(
title: OWSLocalizedString(
"SAFETY_TIPS_NEXT_TIP_BUTTON",
comment: "Button that will show the next safety tip."
),
font: .dynamicTypeBodyClamped.semibold(),
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: self,
selector: #selector(didTapNext))
nextButton.button.setBackgroundImage(UIImage.image(color: .clear), for: .disabled)
nextButton.button.setTitleColor(.ows_accentBlue, for: .disabled)
nextButton.contentEdgeInsets = Constants.buttonEdgeInsets
return nextButton
}()
private lazy var footerView: UIView = {
let stackView = UIStackView(arrangedSubviews: [
previousTipButton,
nextTipButton,
])
let container = UIView()
container.addSubview(stackView)
container.layoutMargins = Constants.footerMargins
container.setContentHuggingHigh()
stackView.axis = .horizontal
stackView.spacing = Constants.footerSpacing
stackView.distribution = .fillEqually
stackView.autoPinEdgesToSuperviewMargins()
return container
}()
// MARK: - TableView
private func buildContents() {
prepareTipsScrollView()
stackView.removeAllSubviews()
stackView.addArrangedSubview(Self.HeaderView(type: type))
stackView.addArrangedSubview(tipScrollView)
stackView.setCustomSpacing(8.0, after: tipScrollView)
stackView.addArrangedSubview(pageControl)
stackView.setCustomSpacing(0.0, after: pageControl)
stackView.addArrangedSubview(UIView.transparentSpacer())
stackView.addArrangedSubview(footerView)
}
private func prepareTipsScrollView() {
var priorView: UIView?
tipScrollView.removeAllSubviews()
SafetyTips.allCases.forEach { tip in
let view = SafetyTipView(safetyTip: tip)
tipScrollView.addSubview(view)
view.autoPinHeight(toHeightOf: tipScrollView)
view.autoPinWidth(toWidthOf: tipScrollView)
if let priorView {
view.autoPinEdge(.leading, to: .trailing, of: priorView)
} else {
view.autoPinEdge(.leading, to: .leading, of: tipScrollView)
}
priorView = view
}
priorView?.autoPinEdge(.trailing, to: .trailing, of: tipScrollView)
}
// MARK: - ScrollViewDelegate
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
updateButtonState()
}
// MARK: - Actions
@objc
private func changePage() {
let x = CGFloat(pageControl.currentPage) * tipScrollView.frame.size.width
tipScrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
updateButtonState()
}
@objc
private func didTapPrevious() {
pageControl.currentPage = max(pageControl.currentPage - 1, 0)
changePage()
}
@objc
private func didTapNext() {
pageControl.currentPage = min(pageControl.currentPage + 1, pageControl.numberOfPages)
changePage()
}
private func updateButtonState() {
switch pageControl.currentPage {
case 0:
previousTipButton.setEnabled(false)
nextTipButton.setEnabled(true)
case pageControl.numberOfPages - 1:
previousTipButton.setEnabled(true)
nextTipButton.setEnabled(false)
default:
previousTipButton.setEnabled(true)
nextTipButton.setEnabled(true)
}
}
private func setColorsForCurrentTheme() {
pageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray20
pageControl.currentPageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray20 : .ows_gray65
}
}
extension SafetyTipsViewController {
class HeaderView: UIView {
private let type: SafetyTipsType
// MARK: Init
init(type: SafetyTipsType) {
self.type = type
super.init(frame: .zero)
layoutMargins = Constants.outerMargins
let stackView = UIStackView()
self.addSubview(stackView)
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = Constants.stackSpacing
stackView.autoPinEdgesToSuperviewMargins()
stackView.addArrangedSubviews([
titleLabel,
subtitleLabel
])
self.setContentHuggingHigh()
updateFontsForCurrentPreferredContentSize()
setColorsForCurrentTheme()
}
@available(*, unavailable, message: "Use other constructor")
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Views
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = OWSLocalizedString(
"SAFETY_TIPS_HEADER_TITLE",
comment: "Title for Safety Tips education screen.")
label.numberOfLines = 0
label.textAlignment = .center
label.lineBreakMode = .byWordWrapping
label.setContentHuggingVerticalHigh()
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
let message = {
switch type {
case .contact:
return OWSLocalizedString(
"SAFETY_TIPS_INDIVIDUAL_HEADER_MESSAGE",
comment: "Message describing safety tips for 1:1 conversations."
)
case .group:
return OWSLocalizedString(
"SAFETY_TIPS_GROUPS_HEADER_MESSAGE",
comment: "Message describing safety tips for group conversations."
)
}
}()
label.text = message
label.numberOfLines = 0
label.textAlignment = .center
label.lineBreakMode = .byWordWrapping
label.setContentHuggingVerticalHigh()
return label
}()
// MARK: - Style views
func updateFontsForCurrentPreferredContentSize() {
titleLabel.font = .dynamicTypeTitle2Clamped.bold()
subtitleLabel.font = .dynamicTypeBody2Clamped
}
func setColorsForCurrentTheme() {
titleLabel.textColor = Theme.primaryTextColor
subtitleLabel.textColor = Theme.primaryTextColor
}
}
}
extension SafetyTipsViewController {
class SafetyTipView: UIView {
private enum Constants {
static let cornerRadius: CGFloat = 20.0
static let layoutMargin: CGFloat = 12.0
static let imageMargin: CGFloat = 24.0
static let containerMargins: UIEdgeInsets = .init(
top: layoutMargin,
left: layoutMargin,
bottom: 24.0,
right: layoutMargin
)
}
fileprivate init(safetyTip: SafetyTips) {
super.init(frame: .zero)
layoutMargins = .init(hMargin: 24.0, vMargin: 0.0)
let containerView = UIView()
containerView.layer.cornerRadius = Constants.cornerRadius
containerView.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_white
self.addSubview(containerView)
containerView.layoutMargins = Constants.containerMargins
containerView.autoPinEdgesToSuperviewMargins()
let stackView = UIStackView()
stackView.axis = .vertical
containerView.addSubview(stackView)
stackView.spacing = Constants.layoutMargin
stackView.autoPinEdgesToSuperviewMargins()
let imageContainerView = UIView()
imageContainerView.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray60 : .ows_gray02
imageContainerView.layer.cornerRadius = Constants.cornerRadius
imageContainerView.layoutMargins = .init(margin: Constants.imageMargin)
stackView.addArrangedSubview(imageContainerView)
let imageView = UIImageView(image: safetyTip.image)
imageView.contentMode = .scaleAspectFit
imageContainerView.addSubview(imageView)
imageView.autoPinEdgesToSuperviewMargins()
imageView.autoPinToAspectRatio(withSize: safetyTip.image?.size ?? .init(square: 1.0))
let titleLabel = UILabel()
titleLabel.text = safetyTip.title
titleLabel.numberOfLines = 0
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.font = .dynamicTypeBody.bold()
titleLabel.textColor = Theme.primaryTextColor
stackView.addArrangedSubview(titleLabel)
let messageLabel = UILabel()
messageLabel.text = safetyTip.body
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.font = .dynamicTypeBodyClamped
messageLabel.textColor = Theme.secondaryTextAndIconColor
stackView.addArrangedSubview(messageLabel)
}
@available(*, unavailable, message: "Use other constructor")
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}