safety tips update

This commit is contained in:
kate-signal 2026-04-15 14:44:38 -04:00 committed by GitHub
parent 89e5757323
commit d08c0ab160
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 519 additions and 357 deletions

View File

@ -279,6 +279,8 @@ public protocol CVComponentDelegate: AnyObject, AudioMessageViewDelegate, CVPoll
func didTapViewPoll(pollInteractionUniqueId: String)
func didTapViewPinnedMessage(pinnedMessageUniqueId: String)
func didTapSafetyTips()
}
// MARK: -

View File

@ -234,8 +234,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
safetyButtonLabelConfig.applyForRendering(button: showTipsButton)
showTipsButton.ows_contentEdgeInsets = .init(hMargin: hPaddingSafetyButton, vMargin: vPaddingSafetyButton)
showTipsButton.dimsWhenHighlighted = true
showTipsButton.block = { [weak self] in
self?.didShowTips(type: safetySection.threadType)
showTipsButton.block = { [weak componentDelegate] in
componentDelegate?.didTapSafetyTips()
}
if conversationStyle.hasWallpaper {
@ -704,7 +704,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
safetySection.shouldShowSafetyTipsButton,
componentView.showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
{
didShowTips(type: safetySection.threadType)
componentDelegate.didTapSafetyTips()
return true
}
@ -808,12 +808,6 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
}
extension CVComponentThreadDetails {
private func didShowTips(type: SafetyTipsType) {
let viewController = SafetyTipsViewController(type: type)
UIApplication.shared.frontmostViewController?.present(viewController, animated: true)
}
private static func buildGroupsSafetySection(
from groupThread: TSGroupThread,
tx: DBReadTransaction,

View File

@ -1449,6 +1449,21 @@ extension ConversationViewController: CVComponentDelegate {
isAnimated: true,
)
}
public func didTapSafetyTips() {
let viewController = SafetyTipsViewController()
viewController.delegate = self
present(viewController, animated: true)
}
}
// MARK: - SafetyTipsViewControllerDelegate
extension ConversationViewController: SafetyTipsViewControllerDelegate {
public func didTapViewMoreSafetyTips() {
let viewController = MoreSafetyTipsViewController()
present(viewController, animated: true)
}
}
// MARK: - OWSNavigationChildController

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "safety_tip1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "safety_tip2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "safety_tip3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "safety_tip4.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "safety_tip5.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_01.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_02.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_03.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_04.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_05.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_240_06.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_48_01.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_48_02.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "safetytip_48_03.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "chevron-left-26.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "chevron-right-26.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -532,6 +532,8 @@ extension EditHistoryTableSheetViewController: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}
extension EditHistoryTableSheetViewController: LongTextViewDelegate {

View File

@ -545,4 +545,6 @@ extension MediaGalleryFileCell: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}

View File

@ -780,4 +780,6 @@ extension MemberLabelViewController: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}

View File

@ -1346,6 +1346,8 @@ extension MessageDetailViewController: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}
extension MessageDetailViewController: UINavigationControllerDelegate {

View File

@ -658,4 +658,6 @@ extension PinnedMessagesDetailsViewController: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}

View File

@ -12,9 +12,194 @@ public enum SafetyTipsType {
case group
}
public protocol SafetyTipsViewControllerDelegate: AnyObject {
func didTapViewMoreSafetyTips()
}
public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
private enum SafetyTips: CaseIterable {
case chatsFromSignal
case reviewNames
case scams
var image: UIImage? {
switch self {
case .chatsFromSignal:
return UIImage(resource: .safetytip4801)
case .reviewNames:
return UIImage(resource: .safetytip4802)
case .scams:
return UIImage(resource: .safetytip4803)
}
}
var title: String {
switch self {
case .chatsFromSignal:
return OWSLocalizedString(
"SAFETY_TIPS_SIGNAL_CHATS_TITLE",
comment: "Message title describing the signal chats tip.",
)
case .reviewNames:
return OWSLocalizedString(
"SAFETY_TIPS_REVIEW_NAMES_TITLE",
comment: "Message title describing the review names safety tip.",
)
case .scams:
return OWSLocalizedString(
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_TITLE",
comment: "Message title describing the scams safety tip.",
)
}
}
var body: String {
switch self {
case .chatsFromSignal:
return OWSLocalizedString(
"SAFETY_TIPS_SIGNAL_CHATS_BODY",
comment: "Message body describing the signal chats tip.",
)
case .reviewNames:
return OWSLocalizedString(
"SAFETY_TIPS_REVIEW_NAMES_BODY",
comment: "Message body describing the review names safety tip.",
)
case .scams:
return OWSLocalizedString(
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_BODY",
comment: "Message body describing the scams safety tip.",
)
}
}
}
let contentScrollView = UIScrollView()
let stackView = UIStackView()
public weak var delegate: SafetyTipsViewControllerDelegate?
override public func viewDidLoad() {
super.viewDidLoad()
minimizedHeight = min(612, CurrentAppContext().frame.height)
super.allowsExpansion = false
let header = UILabel()
header.text = OWSLocalizedString(
"SAFETY_TIPS_HEADER_TITLE",
comment: "Title for Safety Tips education screen.",
)
header.font = .dynamicTypeHeadline
header.textAlignment = .center
header.isAccessibilityElement = true
header.accessibilityTraits.insert(.header)
contentView.addSubview(header)
header.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
header.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
header.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 16),
])
contentView.addSubview(contentScrollView)
contentScrollView.addSubview(stackView)
stackView.axis = .vertical
stackView.spacing = 20
contentScrollView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentScrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 24),
contentScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -90),
contentScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.leadingAnchor, constant: 24),
stackView.trailingAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.trailingAnchor, constant: 24),
stackView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor, constant: -48),
])
for bullet in SafetyTips.allCases {
let bulletView = SafetyBulletView(bullet)
stackView.addArrangedSubview(bulletView)
}
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = UIColor.Signal.secondaryFill
config.cornerStyle = .capsule
var attrString = AttributedString(CommonStrings.viewMoreButton)
attrString.font = .dynamicTypeBodyClamped.medium()
config.attributedTitle = attrString
config.baseForegroundColor = UIColor.Signal.label
config.contentInsets = .init(margin: 14)
let viewMoreButton = UIButton(
configuration: config,
primaryAction: .init(handler: { [weak self] _ in
self?.dismiss(animated: true)
self?.delegate?.didTapViewMoreSafetyTips()
}),
)
viewMoreButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(viewMoreButton)
NSLayoutConstraint.activate([
viewMoreButton.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
viewMoreButton.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 20),
viewMoreButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -20),
viewMoreButton.heightAnchor.constraint(equalToConstant: 52),
])
}
private class SafetyBulletView: UIStackView {
init(_ bullet: SafetyTips) {
super.init(frame: .zero)
self.axis = .horizontal
self.alignment = .firstBaseline
self.spacing = 24
self.isLayoutMarginsRelativeArrangement = true
self.layoutMargins = .zero
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = 8
let headerLabel = UILabel()
headerLabel.text = bullet.title
headerLabel.numberOfLines = 0
headerLabel.textColor = UIColor.Signal.label
headerLabel.font = .dynamicTypeBody.semibold()
textStack.addArrangedSubview(headerLabel)
let bodyLabel = UILabel()
bodyLabel.text = bullet.body
bodyLabel.numberOfLines = 0
bodyLabel.textColor = UIColor.Signal.secondaryLabel
bodyLabel.font = .dynamicTypeBody
textStack.addArrangedSubview(bodyLabel)
let bulletPoint = UIImageView(image: bullet.image)
bulletPoint.contentMode = .scaleAspectFit
bulletPoint.translatesAutoresizingMaskIntoConstraints = false
bulletPoint.widthAnchor.constraint(equalToConstant: 48).isActive = true
bulletPoint.heightAnchor.constraint(equalToConstant: 48).isActive = true
addArrangedSubview(bulletPoint)
addArrangedSubview(textStack)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
public class MoreSafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
let contentScrollView = UIScrollView()
override public var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
override public var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
@ -25,59 +210,47 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
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,
)
static let buttonDiameter: CGFloat = 52.0
static let buttonMargin: CGFloat = 24.0
}
fileprivate enum SafetyTips: CaseIterable {
case fakeNames
case crypto
fileprivate enum MoreSafetyTips: CaseIterable {
case chatsFromSignal
case reviewNames
case vagueMessages
case messagesWithLinks
case crypto
case fakeBusiness
var image: UIImage? {
switch self {
case .fakeNames:
return UIImage(named: "safety-tip-5")
case .crypto:
return UIImage(named: "safety-tip-1")
case .chatsFromSignal:
return UIImage(resource: .safetytip24001)
case .reviewNames:
return UIImage(resource: .safetytip24002)
case .vagueMessages:
return UIImage(named: "safety-tip-2")
return UIImage(resource: .safetytip24003)
case .messagesWithLinks:
return UIImage(named: "safety-tip-3")
return UIImage(resource: .safetytip24004)
case .crypto:
return UIImage(resource: .safetytip24005)
case .fakeBusiness:
return UIImage(named: "safety-tip-4")
return UIImage(resource: .safetytip24006)
}
}
var title: String {
switch self {
case .fakeNames:
case .chatsFromSignal:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_NAMES_TITLE",
comment: "Message title describing the fake names safety tip.",
"SAFETY_TIPS_SIGNAL_CHATS_TITLE",
comment: "Message title describing the signal chats tip.",
)
case .crypto:
case .reviewNames:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_TITLE",
comment: "Message title describing the crypto safety tip.",
"SAFETY_TIPS_REVIEW_NAMES_TITLE",
comment: "Message title describing the review names safety tip.",
)
case .vagueMessages:
return OWSLocalizedString(
@ -89,6 +262,11 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
"SAFETY_TIPS_MESSAGE_LINKS_TITLE",
comment: "Message title describing the safety tip about unknown links in messages.",
)
case .crypto:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_TITLE",
comment: "Message title describing the crypto safety tip.",
)
case .fakeBusiness:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_BUSINESS_TITLE",
@ -99,15 +277,15 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
var body: String {
switch self {
case .fakeNames:
case .chatsFromSignal:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_NAMES_BODY",
comment: "Message contents for the fake names safety tip.",
"SAFETY_TIPS_SIGNAL_CHATS_BODY_VIEW_MORE",
comment: "Message body describing the signal chats tip in the 'view more' flow.",
)
case .crypto:
case .reviewNames:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_BODY",
comment: "Message contents for the crypto safety tip.",
"SAFETY_TIPS_REVIEW_NAMES_BODY_VIEW_MORE",
comment: "Message body describing the review names safety tip in the 'view more' flow.",
)
case .vagueMessages:
return OWSLocalizedString(
@ -119,6 +297,11 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
"SAFETY_TIPS_MESSAGE_LINKS_BODY",
comment: "Message contents for the unknown links in messages safety tip.",
)
case .crypto:
return OWSLocalizedString(
"SAFETY_TIPS_CRYPTO_BODY",
comment: "Message contents for the crypto safety tip.",
)
case .fakeBusiness:
return OWSLocalizedString(
"SAFETY_TIPS_FAKE_BUSINESS_BODY",
@ -128,34 +311,34 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
}
}
public var prefersNavigationBarHidden: Bool { true }
private let type: SafetyTipsType
init(type: SafetyTipsType) {
self.type = type
super.init()
}
var prefersNavigationBarHidden: Bool { true }
override public func viewDidLoad() {
super.viewDidLoad()
minimizedHeight = min(725, CurrentAppContext().frame.height)
minimizedHeight = min(510, CurrentAppContext().frame.height)
super.allowsExpansion = false
contentView.addSubview(contentScrollView)
contentScrollView.addSubview(stackView)
contentScrollView.addSubview(tipScrollView)
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)
tipScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tipScrollView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
tipScrollView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
tipScrollView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
tipScrollView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
tipScrollView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor),
])
contentScrollView.translatesAutoresizingMaskIntoConstraints = false
contentScrollView.autoPinEdgesToSuperviewEdges()
NSLayoutConstraint.activate([
contentScrollView.topAnchor.constraint(equalTo: contentView.topAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -84),
contentScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
contentScrollView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
])
buildContents()
updateButtonState()
@ -181,62 +364,71 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
private lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.numberOfPages = SafetyTips.allCases.count
pageControl.numberOfPages = MoreSafetyTips.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: .dynamicTypeHeadlineClamped,
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
private lazy var previousTipButton: UIButton = {
let previousButton = UIButton(type: .system)
var config = UIButton.Configuration.filled()
config.baseForegroundColor = UIColor.Signal.label
config.baseBackgroundColor = UIColor.Signal.primaryFill
config.image = UIImage(resource: .chevronLeft26)
config.cornerStyle = .capsule
previousButton.accessibilityLabel = CommonStrings.backButton
previousButton.configuration = config
previousButton.addTarget(self, action: #selector(didTapPrevious), for: .touchUpInside)
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: .dynamicTypeHeadlineClamped,
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
private lazy var nextTipButton: UIButton = {
let nextButton = UIButton(type: .system)
var config = UIButton.Configuration.filled()
config.baseForegroundColor = UIColor.Signal.label
config.baseBackgroundColor = UIColor.Signal.primaryFill
config.image = UIImage(resource: .chevronRight26)
config.cornerStyle = .capsule
nextButton.configuration = config
nextButton.accessibilityLabel = CommonStrings.nextButton
nextButton.addTarget(self, action: #selector(didTapNext), for: .touchUpInside)
return nextButton
}()
private lazy var footerView: UIView = {
let stackView = UIStackView(arrangedSubviews: [
previousTipButton,
pageControl,
nextTipButton,
])
nextTipButton.translatesAutoresizingMaskIntoConstraints = false
previousTipButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nextTipButton.widthAnchor.constraint(equalToConstant: Constants.buttonDiameter),
nextTipButton.heightAnchor.constraint(equalToConstant: Constants.buttonDiameter),
previousTipButton.widthAnchor.constraint(equalToConstant: Constants.buttonDiameter),
previousTipButton.heightAnchor.constraint(equalToConstant: Constants.buttonDiameter),
])
let container = UIView()
container.addSubview(stackView)
container.layoutMargins = Constants.footerMargins
container.setContentHuggingHigh()
container.backgroundColor = sheetBackgroundColor
container.tintColor = sheetBackgroundColor
stackView.axis = .horizontal
stackView.spacing = Constants.footerSpacing
stackView.distribution = .fillEqually
stackView.autoPinEdgesToSuperviewMargins()
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
stackView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -Constants.buttonMargin),
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Constants.buttonMargin),
stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Constants.buttonMargin),
])
return container
}()
@ -245,20 +437,21 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
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)
contentView.addSubview(footerView)
footerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
footerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
footerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
footerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
footerView.heightAnchor.constraint(equalToConstant: 84),
])
}
private func prepareTipsScrollView() {
var priorView: UIView?
tipScrollView.removeAllSubviews()
SafetyTips.allCases.forEach { tip in
MoreSafetyTips.allCases.forEach { tip in
let view = SafetyTipView(safetyTip: tip)
tipScrollView.addSubview(view)
@ -289,6 +482,11 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
let x = CGFloat(pageControl.currentPage) * tipScrollView.frame.size.width
tipScrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
updateButtonState()
let currentPageView = tipScrollView.subviews[pageControl.currentPage]
DispatchQueue.main.async {
UIAccessibility.post(notification: .layoutChanged, argument: currentPageView)
}
}
@objc
@ -306,14 +504,23 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
private func updateButtonState() {
switch pageControl.currentPage {
case 0:
previousTipButton.setEnabled(false)
nextTipButton.setEnabled(true)
// hide previous, show next
previousTipButton.alpha = 0
previousTipButton.isUserInteractionEnabled = false
nextTipButton.alpha = 1
nextTipButton.isUserInteractionEnabled = true
case pageControl.numberOfPages - 1:
previousTipButton.setEnabled(true)
nextTipButton.setEnabled(false)
// show previous, hide next
previousTipButton.alpha = 1
previousTipButton.isUserInteractionEnabled = true
nextTipButton.alpha = 0
nextTipButton.isUserInteractionEnabled = false
default:
previousTipButton.setEnabled(true)
nextTipButton.setEnabled(true)
// show previous, show next
previousTipButton.alpha = 1
previousTipButton.isUserInteractionEnabled = true
nextTipButton.alpha = 1
nextTipButton.isUserInteractionEnabled = true
}
}
@ -323,136 +530,22 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
}
}
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 = .dynamicTypeSubheadlineClamped
}
func setColorsForCurrentTheme() {
titleLabel.textColor = Theme.primaryTextColor
subtitleLabel.textColor = Theme.primaryTextColor
}
}
}
extension SafetyTipsViewController {
extension MoreSafetyTipsViewController {
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) {
fileprivate init(safetyTip: MoreSafetyTips) {
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
self.addSubview(stackView)
stackView.spacing = 12.0
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))
stackView.addArrangedSubview(imageView)
let titleLabel = UILabel()
titleLabel.text = safetyTip.title

View File

@ -560,4 +560,6 @@ extension MockConversationView: CVComponentDelegate {
func didTapVoteOnPoll(poll: OWSPoll, optionIndex: UInt32, isUnvote: Bool) {}
func didTapViewPinnedMessage(pinnedMessageUniqueId: String) {}
func didTapSafetyTips() {}
}

View File

@ -7925,34 +7925,40 @@
"SAFETY_TIPS_FAKE_BUSINESS_BODY" = "Be careful of businesses or government agencies contacting you. Messages involving tax agencies, couriers, and more can be spam.";
/* Message title describing the safety tip about unknown or fake businesses. */
"SAFETY_TIPS_FAKE_BUSINESS_TITLE" = "Fake businesses and institutions";
/* Message contents for the fake names safety tip. */
"SAFETY_TIPS_FAKE_NAMES_BODY" = "Signal will never contact you for your registration code or PIN. Be cautious of requests that impersonate others. Profile names are chosen by their account holder and aren't verified.";
/* Message title describing the fake names safety tip. */
"SAFETY_TIPS_FAKE_NAMES_TITLE" = "Fake names and accounts";
/* Message describing safety tips for group conversations. */
"SAFETY_TIPS_GROUPS_HEADER_MESSAGE" = "Review this request carefully. None of your contacts or people you chat with are in this group. Here are a few things to watch out for:";
"SAFETY_TIPS_FAKE_BUSINESS_TITLE" = "Fake businesses";
/* Title for Safety Tips education screen. */
"SAFETY_TIPS_HEADER_TITLE" = "Safety Tips";
/* Message describing safety tips for 1:1 conversations. */
"SAFETY_TIPS_INDIVIDUAL_HEADER_MESSAGE" = "Be careful when accepting message requests from people you dont know. Watch out for:";
/* Message body describing the scams safety tip. */
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_BODY" = "Avoid vague messages that try to get you to reply. Be aware of financial tips and suspicious web links.";
/* Message title describing the scams safety tip. */
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_TITLE" = "Look out for scams";
/* Message contents for the unknown links in messages safety tip. */
"SAFETY_TIPS_MESSAGE_LINKS_BODY" = "Be careful of messages from people you dont know that have links to websites. Never visit links from people you dont trust.";
/* Message title describing the safety tip about unknown links in messages. */
"SAFETY_TIPS_MESSAGE_LINKS_TITLE" = "Messages with links";
"SAFETY_TIPS_MESSAGE_LINKS_TITLE" = "Messages with web links";
/* Button that will show the next safety tip. */
"SAFETY_TIPS_NEXT_TIP_BUTTON" = "Next";
/* Message body describing the review names safety tip. */
"SAFETY_TIPS_REVIEW_NAMES_BODY" = "Look out for the “Name not verified” notice. Everyone sets their own profile name in Signal.";
/* Button that will show the previous safety tip. */
"SAFETY_TIPS_PREVIOUS_TIP_BUTTON" = "Previous";
/* Message body describing the review names safety tip in the 'view more' flow. */
"SAFETY_TIPS_REVIEW_NAMES_BODY_VIEW_MORE" = "Look out for the “Name not verified” notice. Everyone sets their own profile name and photo in Signal. If youre unsure who a new request is from, its safer to ignore it.";
/* Message title describing the review names safety tip. */
"SAFETY_TIPS_REVIEW_NAMES_TITLE" = "Review names and photos";
/* Message body describing the signal chats tip. */
"SAFETY_TIPS_SIGNAL_CHATS_BODY" = "Signal will never message you for your registration code, PIN, or recovery key. Never respond to a chat pretending to be Signal.";
/* Message body describing the signal chats tip in the 'view more' flow. */
"SAFETY_TIPS_SIGNAL_CHATS_BODY_VIEW_MORE" = "Signal will never message you for your registration code, PIN, or recovery key. Dont reply to chats pretending to be Signal or Signal Support. Bad actors set up fake names to try to take over your account.";
/* Message title describing the signal chats tip. */
"SAFETY_TIPS_SIGNAL_CHATS_TITLE" = "Dont respond to chats from Signal";
/* Message contents for the vague message safety tip. */
"SAFETY_TIPS_VAGUE_MESSAGE_BODY" = "Spammers often start with a simple message like “Hi” to draw you in. If you respond they may engage you further.";
@ -10102,6 +10108,9 @@
/* Button label for the 'View Contact' button */
"VIEW_CONTACT_BUTTON" = "View Contact";
/* Label for the 'view more' button */
"VIEW_MORE_BUTTON" = "View More";
/* Toast alert text shown when tapping on a view-once message that has already been viewed. */
"VIEW_ONCE_ALREADY_VIEWED_TOAST" = "You already viewed this message.";

View File

@ -234,6 +234,10 @@ public enum CommonStrings {
OWSLocalizedString("BUTTON_VIEW", comment: "Label for the 'view' button.")
}
public static var viewMoreButton: String {
OWSLocalizedString("VIEW_MORE_BUTTON", comment: "Label for the 'view more' button")
}
public static var seeAllButton: String {
OWSLocalizedString("SEE_ALL_BUTTON", comment: "Label for the 'see all' button.")
}