466 lines
16 KiB
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")
|
|
}
|
|
}
|
|
}
|