Signal-iOS/SignalUI/ViewControllers/Safety Numbers/MultiFingerprintViewController.swift
2023-07-05 16:29:12 -07:00

800 lines
31 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import Lottie
import PureLayout
import SafariServices
import SignalMessaging
import SignalServiceKit
import UIKit
public class MultiFingerprintViewController: OWSViewController, OWSNavigationChildController {
public var preferredNavigationBarStyle: OWSNavigationBarStyle {
return .solid
}
public var navbarBackgroundColorOverride: UIColor? {
return Self.backgroundColor
}
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
private let recipientAddress: SignalServiceAddress
private let recipientIdentity: OWSRecipientIdentity
private let contactName: String
private let identityKey: IdentityKey
private let fingerprints: [OWSFingerprint]
private var selectedIndex: Int
public init(
fingerprints: [OWSFingerprint],
defaultIndex: Int,
recipientAddress: SignalServiceAddress,
recipientIdentity: OWSRecipientIdentity
) {
self.recipientAddress = recipientAddress
self.contactName = SSKEnvironment.shared.contactsManagerRef.displayName(for: recipientAddress)
// By capturing the identity key when we enter these views, we prevent the edge case
// where the user verifies a key that we learned about while this view was open.
self.recipientIdentity = recipientIdentity
self.identityKey = recipientIdentity.identityKey
self.fingerprints = fingerprints
self.selectedIndex = defaultIndex
super.init()
title = NSLocalizedString("PRIVACY_VERIFICATION_TITLE", comment: "Navbar title")
navigationItem.leftBarButtonItem = .init(
barButtonSystemItem: .done,
target: self, action: #selector(didTapDone),
accessibilityIdentifier: "FingerprintViewController.done"
)
identityStateChangeObserver = NotificationCenter.default.addObserver(
forName: .identityStateDidChange,
object: nil,
queue: .main) { [weak self] _ in
self?.identityStateDidChange()
}
}
deinit {
if let identityStateChangeObserver {
NotificationCenter.default.removeObserver(identityStateChangeObserver)
}
}
private static var backgroundColor: UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray02
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Self.backgroundColor
configureUI()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
scrollToSelectedIndex(animated: false)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
// Its fine to not re-read the value in the write tx; stakes are low.
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
}
}
public override func themeDidChange() {
super.themeDidChange()
view.backgroundColor = Self.backgroundColor
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
// MARK: UI
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
let textView = LinkingTextView()
textView.delegate = self
return textView
}()
private func setSafetyNumbersUpdateTextViewText() {
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
comment: "Header informing the user about the transition from phone number to user identifier based."
),
"\n",
learnMoreString
]).styled(
with: .font(.dynamicTypeFootnote),
.color(Theme.secondaryTextAndIconColor)
)
safetyNumbersUpdateTextView.linkTextAttributes = [
.foregroundColor: Theme.primaryTextColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
}
private lazy var safetyNumbersUpdateView: UIView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 16
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
imageView.autoSetDimensions(to: .square(48))
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
return stackView
}()
private lazy var fingerprintCards: [FingerprintCard] = {
return fingerprints.map { fingerprint in
return FingerprintCard(fingerprint: fingerprint, controller: self)
}
}()
private lazy var fingerprintCarousel: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.isDirectionalLockEnabled = true
scrollView.alwaysBounceVertical = false
scrollView.alwaysBounceHorizontal = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
var xOffset: CGFloat = Constants.cardHInset
var previousView: UIView = scrollView
var nextEdge: ALEdge = .leading
for fingerprintCard in fingerprintCards {
scrollView.addSubview(fingerprintCard)
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
previousView = fingerprintCard
xOffset = Constants.interCardSpacing
nextEdge = .trailing
}
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
scrollView.delegate = self
return scrollView
}()
private lazy var fingerprintCarouselPageControl: UIPageControl = {
let control = UIPageControl()
control.numberOfPages = fingerprints.count
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
return control
}()
private func setCarouselPageControlColors() {
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
}
private lazy var instructionsTextView: UITextView = {
let textView = LinkingTextView()
textView.delegate = self
return textView
}()
private func setInstructionsText() {
let instructionsFormat = OWSLocalizedString(
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS",
comment: "Instructions for verifying your safety number. Embeds {{contact's name}}"
)
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
instructionsTextView.attributedText = NSAttributedString.composed(of: [
String(format: instructionsFormat, contactName),
" ",
learnMoreString
]).styled(
with: .font(.dynamicTypeFootnote),
.color(Theme.secondaryTextAndIconColor),
.alignment(.center)
)
instructionsTextView.linkTextAttributes = [
.foregroundColor: Theme.primaryTextColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
}
private lazy var verifyUnverifyButtonLabel = UILabel()
private lazy var verifyUnverifyPillbox = PillBoxView()
private lazy var verifyUnverifyButton: UIView = {
verifyUnverifyPillbox.layer.masksToBounds = true
verifyUnverifyPillbox.accessibilityIdentifier = "FingerprintViewController.verifyUnverifyButton"
verifyUnverifyPillbox.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapVerifyUnverify)))
verifyUnverifyButtonLabel.font = .systemFont(ofSize: 13, weight: .bold)
verifyUnverifyButtonLabel.textAlignment = .center
verifyUnverifyButtonLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
verifyUnverifyPillbox.addSubview(verifyUnverifyButtonLabel)
verifyUnverifyButtonLabel.autoPinWidthToSuperview(withMargin: 24)
verifyUnverifyButtonLabel.autoPinHeightToSuperview(withMargin: 12)
return verifyUnverifyPillbox
}()
private func setVerifyUnverifyButtonColors() {
verifyUnverifyButtonLabel.textColor = Theme.primaryTextColor
verifyUnverifyPillbox.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .white
}
private func configureUI() {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let containerView = UIView()
view.addSubview(scrollView)
scrollView.addSubview(containerView)
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
containerView.autoPinEdges(toEdgesOf: scrollView)
containerView.autoPinWidth(toWidthOf: view)
containerView.addSubview(safetyNumbersUpdateView)
containerView.addSubview(fingerprintCarousel)
containerView.addSubview(fingerprintCarouselPageControl)
containerView.addSubview(instructionsTextView)
view.addSubview(verifyUnverifyButton)
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
fingerprintCards.forEach {
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
}
fingerprintCarouselPageControl.autoHCenterInSuperview()
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
verifyUnverifyButton.autoHCenterInSuperview()
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
if fingerprints.count <= 1 {
safetyNumbersUpdateView.isHidden = true
fingerprintCarouselPageControl.isHidden = true
scrollView.isScrollEnabled = false
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
} else {
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
}
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
private func updateVerificationStateLabel() {
owsAssertBeta(recipientAddress.isValid)
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress) == .verified
if isVerified {
verifyUnverifyButtonLabel.text = NSLocalizedString(
"PRIVACY_UNVERIFY_BUTTON",
comment: "Button that lets user mark another user's identity as unverified."
)
} else {
verifyUnverifyButtonLabel.text = OWSLocalizedString(
"PRIVACY_VERIFY_BUTTON",
comment: "Button that lets user mark another user's identity as verified."
)
}
view.setNeedsLayout()
}
// MARK: - Fingerprint Card
class FingerprintCard: UIView {
private let fingerprint: OWSFingerprint
private weak var controller: MultiFingerprintViewController?
init(fingerprint: OWSFingerprint, controller: MultiFingerprintViewController) {
self.fingerprint = fingerprint
self.controller = controller
super.init(frame: .zero)
layer.cornerRadius = Constants.cornerRadius
self.backgroundColor = {
switch fingerprint.source {
case .aci: return UIColor(rgbHex: 0x506ecd)
case .e164: return UIColor(rgbHex: 0xdeddda)
}
}()
addSubview(shareButton)
addSubview(qrCodeView)
addSubview(safetyNumberLabel)
shareButton.autoPinEdge(.top, to: .top, of: self, withOffset: 16)
shareButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -16)
qrCodeView.autoPinEdge(.top, to: .bottom, of: shareButton, withOffset: 8)
qrCodeView.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(44, 64))
qrCodeView.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(44, 64))
safetyNumberLabel.autoPinEdge(.top, to: .bottom, of: qrCodeView, withOffset: 30)
safetyNumberLabel.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35))
safetyNumberLabel.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35))
safetyNumberLabel.autoPinEdge(.bottom, to: .bottom, of: self, withOffset: -.scaleFromIPhone5To7Plus(27, 47))
}
required init?(coder: NSCoder) {
fatalError()
}
private lazy var shareButton: UIButton = {
let button = UIButton()
let tintColor: UIColor
switch fingerprint.source {
case .aci:
tintColor = .white
case .e164:
tintColor = .black
}
button.setTemplateImage(
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
tintColor: tintColor
)
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
return button
}()
private lazy var qrCodeView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .white
containerView.layer.cornerRadius = Constants.cornerRadius
containerView.layer.masksToBounds = true
let fingerprintImageView = UIImageView()
fingerprintImageView.image = fingerprint.image
// Don't antialias QR Codes.
fingerprintImageView.layer.magnificationFilter = .nearest
fingerprintImageView.layer.minificationFilter = .nearest
fingerprintImageView.setCompressionResistanceLow()
containerView.addSubview(fingerprintImageView)
fingerprintImageView.autoPin(toAspectRatio: 1)
fingerprintImageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(margin: 20), excludingEdge: .bottom)
let scanLabel = UILabel()
scanLabel.text = NSLocalizedString("PRIVACY_TAP_TO_SCAN", comment: "Button that shows the 'scan with camera' view.")
scanLabel.font = .systemFont(ofSize: .scaleFromIPhone5To7Plus(13, 15))
scanLabel.textColor = Theme.lightThemeSecondaryTextAndIconColor
containerView.addSubview(scanLabel)
scanLabel.autoHCenterInSuperview()
scanLabel.autoPinEdge(.top, to: .bottom, of: fingerprintImageView, withOffset: 12)
scanLabel.autoPinEdge(.bottom, to: .bottom, of: containerView, withOffset: -14)
containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToScan)))
return containerView
}()
private lazy var safetyNumberLabel: UILabel = {
let label = UILabel()
label.text = fingerprint.displayableText
label.font = UIFont(name: "Menlo-Regular", size: 23)
label.textAlignment = .center
switch fingerprint.source {
case .aci:
label.textColor = .white
case .e164:
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
}
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingTail
label.adjustsFontSizeToFitWidth = true
label.isUserInteractionEnabled = true
label.accessibilityIdentifier = "FingerprintViewController.fingerprintLabel"
return label
}()
@objc
func didTapToScan() {
controller?.didTapToScan()
}
@objc
func didTapShare() {
controller?.shareFingerprint(from: shareButton)
}
enum Constants {
static let cornerRadius: CGFloat = 18
}
}
// MARK: PillBoxView
class PillBoxView: UIView {
override var bounds: CGRect {
didSet {
self.layer.cornerRadius = bounds.height / 2
}
}
}
// MARK: - Transition Sheet
private lazy var kvStore: KeyValueStore = {
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
}()
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
}
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
}
private func showTransitionSheet(_ tx: DBWriteTransaction) {
self.setHasShownTransitionSheet(tx)
tx.addAsyncCompletion(on: DispatchQueue.main) {
let sheet = TransitionSheetViewController(parent: self)
self.present(sheet, animated: true)
}
}
class TransitionSheetViewController: InteractiveSheetViewController {
let contentScrollView = UIScrollView()
let stackView = UIStackView()
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
private weak var parentVc: MultiFingerprintViewController?
init(parent: MultiFingerprintViewController) {
self.parentVc = parent
super.init()
}
override public func viewDidLoad() {
super.viewDidLoad()
minimizedHeight = 600
super.allowsExpansion = true
contentView.addSubview(contentScrollView)
stackView.axis = .vertical
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
stackView.spacing = 16
stackView.isLayoutMarginsRelativeArrangement = true
contentScrollView.addSubview(stackView)
stackView.autoPinHeightToSuperview()
// Pin to the scroll view's viewport, not to its scrollable area
stackView.autoPinWidth(toWidthOf: contentScrollView)
contentScrollView.autoPinEdgesToSuperviewEdges()
contentScrollView.alwaysBounceVertical = true
buildContents()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
animationView.play { [weak self] success in
guard success else { return }
self?.loopAnimation()
}
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
animationView.stop()
}
}
private func loopAnimation() {
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
guard success else { return }
self?.loopAnimation()
})
}
private lazy var animationView: AnimationView = {
let animationView = AnimationView(name: "safety-numbers")
animationView.contentMode = .scaleAspectFit
animationView.isUserInteractionEnabled = false
animationView.backgroundColor = .white
animationView.layer.cornerRadius = 12
animationView.layer.masksToBounds = true
return animationView
}()
private func buildContents() {
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
titleLabel.text = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
)
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel)
let paragraphs: [String] = [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
comment: "Informs the user about the transition from phone number to user identifier based."
),
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
comment: "Informs the user about the transition from phone number to user identifier based."
)
]
var lastParagraphLabel: UILabel!
for paragraph in paragraphs {
let paragraphLabel = UILabel()
paragraphLabel.text = paragraph
paragraphLabel.textAlignment = .natural
paragraphLabel.font = .dynamicTypeSubheadlineClamped
paragraphLabel.numberOfLines = 0
paragraphLabel.lineBreakMode = .byWordWrapping
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
stackView.addArrangedSubview(paragraphLabel)
lastParagraphLabel = paragraphLabel
}
stackView.setCustomSpacing(20, after: lastParagraphLabel)
stackView.addArrangedSubview(animationView)
stackView.setCustomSpacing(24, after: animationView)
animationView.autoPinWidth(toWidthOf: self.view, offset: -48)
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
let learnMoreLabel = UILabel()
learnMoreLabel.text = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
)
learnMoreLabel.textAlignment = .center
learnMoreLabel.font = .dynamicTypeBody
learnMoreLabel.textColor = Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link
learnMoreLabel.isUserInteractionEnabled = true
learnMoreLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapLearnMore)))
stackView.addArrangedSubview(learnMoreLabel)
stackView.setCustomSpacing(30, after: learnMoreLabel)
let continueButton = OWSButton(
title: OWSLocalizedString(
"ALERT_ACTION_ACKNOWLEDGE",
comment: "generic button text to acknowledge that the corresponding text was read."
)
) { [weak self] in
self?.dismiss(animated: true)
}
continueButton.layer.cornerRadius = 16
continueButton.backgroundColor = .ows_accentBlue
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
stackView.addArrangedSubview(continueButton)
}
@objc
func didTapLearnMore() {
self.dismiss(animated: true) { [weak self] in
self?.parentVc?.didTapLearnMore()
}
}
}
// MARK: Actions
@objc
private func didTapDone() {
dismiss(animated: true)
}
private func didTapLearnMore() {
let learnMoreUrl = URL(string: "https://support.signal.org/hc/articles/213134107")!
let safariVC = SFSafariViewController(url: learnMoreUrl)
present(safariVC, animated: true)
}
@objc
private func didUpdatePageControl() {
self.selectedIndex = fingerprintCarouselPageControl.currentPage
scrollToSelectedIndex()
}
@objc
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
guard gestureRecognizer.state == .recognized else { return }
databaseStorage.write { transaction in
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress, transaction: transaction) == .verified
let newVerificationState: OWSVerificationState = isVerified ? .default : .verified
OWSIdentityManager.shared.setVerificationState(
newVerificationState,
identityKey: identityKey,
address: recipientAddress,
isUserInitiatedChange: true,
transaction: transaction
)
}
dismiss(animated: true)
}
private func shareFingerprint(from fromView: UIView) {
let fingerprint = fingerprints[selectedIndex]
Logger.debug("Sharing safety numbers")
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
let shareFormat = NSLocalizedString(
"SAFETY_NUMBER_SHARE_FORMAT",
comment: "Snippet to share {{safety number}} with a friend. sent e.g. via SMS"
)
let shareString = String(format: shareFormat, fingerprint.displayableText)
let activityController = UIActivityViewController(
activityItems: [ shareString ],
applicationActivities: [ compareActivity ]
)
if let popoverPresentationController = activityController.popoverPresentationController {
popoverPresentationController.sourceView = fromView
}
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
let iCloudActivityType = "com.apple.CloudDocsUI.AddToiCloudDrive"
activityController.excludedActivityTypes = [
.postToFacebook,
.postToWeibo,
.airDrop,
.postToTwitter,
.init(rawValue: iCloudActivityType) // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
]
present(activityController, animated: true)
}
fileprivate func didTapToScan() {
let viewController = FingerprintScanViewController(
recipientAddress: recipientAddress,
recipientIdentity: recipientIdentity,
fingerprints: .multiFingerprint(self.fingerprints, defaultIndex: self.selectedIndex)
)
navigationController?.pushViewController(viewController, animated: true)
}
private func scrollToSelectedIndex(animated: Bool = true) {
let xOffset: CGFloat
if selectedIndex == 0 {
xOffset = 0
} else {
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
}
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
}
// MARK: Notifications
private var identityStateChangeObserver: Any?
private func identityStateDidChange() {
AssertIsOnMainThread()
updateVerificationStateLabel()
}
// MARK: - Constants
enum Constants {
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
static var interCardSpacing: CGFloat = cardHInset / 2
// Link doesn't matter, we will override tap behavior.
static let transitionLearnMoreUrl = "https://support.signal.org/"
static let learnMoreUrl = "https://support.signal.org/learnMore"
}
}
extension MultiFingerprintViewController: CompareSafetyNumbersActivityDelegate {
public func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity) {
FingerprintScanViewController.showVerificationSucceeded(
from: self,
identityKey: identityKey,
recipientAddress: recipientAddress,
contactName: contactName,
tag: logTag
)
}
public func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error) {
let isUserError = (error as NSError).code == OWSErrorCode.userError.rawValue
FingerprintScanViewController.showVerificationFailed(
from: self,
isUserError: isUserError,
localizedErrorDescription: error.userErrorDescription,
tag: logTag
)
}
}
extension MultiFingerprintViewController: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == Constants.transitionLearnMoreUrl {
DependenciesBridge.shared.db.write {
self.showTransitionSheet($0)
}
} else if URL.absoluteString == Constants.learnMoreUrl {
self.didTapLearnMore()
}
return false
}
}
extension MultiFingerprintViewController: UIScrollViewDelegate {
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
self.selectedIndex = selectedIndex
self.fingerprintCarouselPageControl.currentPage = selectedIndex
}
}