final class NewPrivateChatVC : BaseVC , UIPageViewControllerDataSource , UIPageViewControllerDelegate , OWSQRScannerDelegate {
private let pageVC = UIPageViewController ( transitionStyle : . scroll , navigationOrientation : . horizontal , options : nil )
private var pages : [ UIViewController ] = [ ]
private var targetVCIndex : Int ?
// MARK: C o m p o n e n t s
private lazy var tabBar : TabBar = {
let tabs = [
TabBar . Tab ( title : NSLocalizedString ( " vc_create_private_chat_enter_session_id_tab_title " , comment : " " ) ) { [ weak self ] in
guard let self = self else { return }
self . pageVC . setViewControllers ( [ self . pages [ 0 ] ] , direction : . forward , animated : false , completion : nil )
} ,
TabBar . Tab ( title : NSLocalizedString ( " vc_create_private_chat_scan_qr_code_tab_title " , comment : " " ) ) { [ weak self ] in
guard let self = self else { return }
self . pageVC . setViewControllers ( [ self . pages [ 1 ] ] , direction : . forward , animated : false , completion : nil )
}
]
return TabBar ( tabs : tabs )
} ( )
private lazy var enterPublicKeyVC : EnterPublicKeyVC = {
let result = EnterPublicKeyVC ( )
result . newPrivateChatVC = self
return result
} ( )
private lazy var scanQRCodePlaceholderVC : ScanQRCodePlaceholderVC = {
let result = ScanQRCodePlaceholderVC ( )
result . newPrivateChatVC = self
return result
} ( )
private lazy var scanQRCodeWrapperVC : ScanQRCodeWrapperVC = {
let message = NSLocalizedString ( " vc_create_private_chat_scan_qr_code_explanation " , comment : " " )
let result = ScanQRCodeWrapperVC ( message : message )
result . delegate = self
return result
} ( )
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
setUpGradientBackground ( )
setUpNavBarStyle ( )
setNavBarTitle ( NSLocalizedString ( " vc_create_private_chat_title " , comment : " " ) )
let navigationBar = navigationController ! . navigationBar
// S e t u p n a v i g a t i o n b a r b u t t o n s
let closeButton = UIBarButtonItem ( image : # imageLiteral ( resourceName : " X " ) , style : . plain , target : self , action : #selector ( close ) )
closeButton . tintColor = Colors . text
navigationItem . leftBarButtonItem = closeButton
// S e t u p p a g e V C
let hasCameraAccess = ( AVCaptureDevice . authorizationStatus ( for : . video ) = = . authorized )
pages = [ enterPublicKeyVC , ( hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC ) ]
pageVC . dataSource = self
pageVC . delegate = self
pageVC . setViewControllers ( [ enterPublicKeyVC ] , direction : . forward , animated : false , completion : nil )
// S e t u p t a b b a r
view . addSubview ( tabBar )
tabBar . pin ( . leading , to : . leading , of : view )
let tabBarInset : CGFloat
if #available ( iOS 13 , * ) {
tabBarInset = navigationBar . height ( )
} else {
tabBarInset = 0
}
tabBar . pin ( . top , to : . top , of : view , withInset : tabBarInset )
view . pin ( . trailing , to : . trailing , of : tabBar )
// S e t u p p a g e V C c o n s t r a i n t s
let pageVCView = pageVC . view !
view . addSubview ( pageVCView )
pageVCView . pin ( . leading , to : . leading , of : view )
pageVCView . pin ( . top , to : . bottom , of : tabBar )
view . pin ( . trailing , to : . trailing , of : pageVCView )
view . pin ( . bottom , to : . bottom , of : pageVCView )
let screen = UIScreen . main . bounds
pageVCView . set ( . width , to : screen . width )
let height : CGFloat
if #available ( iOS 13 , * ) {
height = navigationController ! . view . bounds . height - navigationBar . height ( ) - TabBar . snHeight
} else {
let statusBarHeight = UIApplication . shared . statusBarFrame . height
height = navigationController ! . view . bounds . height - navigationBar . height ( ) - TabBar . snHeight - statusBarHeight
}
pageVCView . set ( . height , to : height )
enterPublicKeyVC . constrainHeight ( to : height )
scanQRCodePlaceholderVC . constrainHeight ( to : height )
}
// MARK: G e n e r a l
func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? {
guard let index = pages . firstIndex ( of : viewController ) , index != 0 else { return nil }
return pages [ index - 1 ]
}
func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? {
guard let index = pages . firstIndex ( of : viewController ) , index != ( pages . count - 1 ) else { return nil }
return pages [ index + 1 ]
}
fileprivate func handleCameraAccessGranted ( ) {
pages [ 1 ] = scanQRCodeWrapperVC
pageVC . setViewControllers ( [ scanQRCodeWrapperVC ] , direction : . forward , animated : false , completion : nil )
}
// MARK: U p d a t i n g
func pageViewController ( _ pageViewController : UIPageViewController , willTransitionTo pendingViewControllers : [ UIViewController ] ) {
guard let targetVC = pendingViewControllers . first , let index = pages . firstIndex ( of : targetVC ) else { return }
targetVCIndex = index
}
func pageViewController ( _ pageViewController : UIPageViewController , didFinishAnimating isFinished : Bool , previousViewControllers : [ UIViewController ] , transitionCompleted isCompleted : Bool ) {
guard isCompleted , let index = targetVCIndex else { return }
tabBar . selectTab ( at : index )
}
// MARK: I n t e r a c t i o n
@objc private func close ( ) {
dismiss ( animated : true , completion : nil )
}
func controller ( _ controller : OWSQRCodeScanningViewController , didDetectQRCodeWith string : String ) {
let hexEncodedPublicKey = string
startNewPrivateChatIfPossible ( with : hexEncodedPublicKey )
}
fileprivate func startNewPrivateChatIfPossible ( with hexEncodedPublicKey : String ) {
if ! ECKeyPair . isValidHexEncodedPublicKey ( candidate : hexEncodedPublicKey ) {
let alert = UIAlertController ( title : NSLocalizedString ( " invalid_session_id " , comment : " " ) , message : NSLocalizedString ( " Please check the Session ID and try again " , comment : " " ) , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " OK " , comment : " " ) , style : . default , handler : nil ) )
presentAlert ( alert )
} else {
let thread = TSContactThread . getOrCreateThread ( contactId : hexEncodedPublicKey )
presentingViewController ? . dismiss ( animated : true , completion : nil )
SignalApp . shared ( ) . presentConversation ( for : thread , action : . compose , animated : false )
}
}
}
private final class EnterPublicKeyVC : UIViewController {
weak var newPrivateChatVC : NewPrivateChatVC !
private var isKeyboardShowing = false
private var bottomConstraint : NSLayoutConstraint !
// MARK: C o m p o n e n t s
private let publicKeyTextView = TextView ( placeholder : NSLocalizedString ( " vc_enter_public_key_text_field_hint " , comment : " " ) )
private lazy var copyButton : Button = {
let result = Button ( style : . unimportant , size : . medium )
result . setTitle ( NSLocalizedString ( " copy " , comment : " " ) , for : UIControl . State . normal )
result . addTarget ( self , action : #selector ( copyPublicKey ) , for : UIControl . Event . touchUpInside )
return result
} ( )
private lazy var userPublicKeyLabel : UILabel = {
let result = UILabel ( )
result . textColor = Colors . text
result . font = Fonts . spaceMono ( ofSize : Values . mediumFontSize )
result . numberOfLines = 0
result . textAlignment = . center
result . lineBreakMode = . byCharWrapping
result . text = getUserHexEncodedPublicKey ( )
return result
} ( )
private lazy var spacer1 = UIView . spacer ( withHeight : Values . largeSpacing )
private lazy var spacer2 = UIView . spacer ( withHeight : Values . largeSpacing )
private lazy var spacer3 = UIView . spacer ( withHeight : Values . largeSpacing )
private lazy var separator = Separator ( title : NSLocalizedString ( " your_session_id " , comment : " " ) )
private lazy var buttonContainer : UIStackView = {
let result = UIStackView ( )
result . axis = . horizontal
result . spacing = Values . mediumSpacing
result . distribution = . fillEqually
return result
} ( )
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
// R e m o v e b a c k g r o u n d c o l o r
view . backgroundColor = . clear
// E x p l a n a t i o n l a b e l
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text . withAlphaComponent ( Values . mediumOpacity )
explanationLabel . font = . systemFont ( ofSize : Values . verySmallFontSize )
explanationLabel . text = NSLocalizedString ( " vc_enter_public_key_explanation " , comment : " " )
explanationLabel . numberOfLines = 0
explanationLabel . textAlignment = . center
explanationLabel . lineBreakMode = . byWordWrapping
// S h a r e b u t t o n
let shareButton = Button ( style : . unimportant , size : . medium )
shareButton . setTitle ( NSLocalizedString ( " share " , comment : " " ) , for : UIControl . State . normal )
shareButton . addTarget ( self , action : #selector ( sharePublicKey ) , for : UIControl . Event . touchUpInside )
// B u t t o n c o n t a i n e r
buttonContainer . addArrangedSubview ( copyButton )
buttonContainer . addArrangedSubview ( shareButton )
// N e x t b u t t o n
let nextButton = Button ( style : . prominentOutline , size : . large )
nextButton . setTitle ( NSLocalizedString ( " next " , comment : " " ) , for : UIControl . State . normal )
nextButton . addTarget ( self , action : #selector ( startNewPrivateChatIfPossible ) , for : UIControl . Event . touchUpInside )
let nextButtonContainer = UIView ( )
nextButtonContainer . addSubview ( nextButton )
nextButton . pin ( . leading , to : . leading , of : nextButtonContainer , withInset : 80 )
nextButton . pin ( . top , to : . top , of : nextButtonContainer )
nextButtonContainer . pin ( . trailing , to : . trailing , of : nextButton , withInset : 80 )
nextButtonContainer . pin ( . bottom , to : . bottom , of : nextButton )
// M a i n s t a c k v i e w
let mainStackView = UIStackView ( arrangedSubviews : [ publicKeyTextView , UIView . spacer ( withHeight : Values . smallSpacing ) , explanationLabel , spacer1 , separator , spacer2 , userPublicKeyLabel , spacer3 , buttonContainer , UIView . vStretchingSpacer ( ) , nextButtonContainer ] )
mainStackView . axis = . vertical
mainStackView . alignment = . fill
mainStackView . layoutMargins = UIEdgeInsets ( top : Values . largeSpacing , left : Values . largeSpacing , bottom : Values . largeSpacing , right : Values . largeSpacing )
mainStackView . isLayoutMarginsRelativeArrangement = true
view . addSubview ( mainStackView )
mainStackView . pin ( . leading , to : . leading , of : view )
mainStackView . pin ( . top , to : . top , of : view )
view . pin ( . trailing , to : . trailing , of : mainStackView )
bottomConstraint = view . pin ( . bottom , to : . bottom , of : mainStackView )
// W i d t h c o n s t r a i n t
view . set ( . width , to : UIScreen . main . bounds . width )
// D i s m i s s k e y b o a r d o n t a p
let tapGestureRecognizer = UITapGestureRecognizer ( target : self , action : #selector ( dismissKeyboard ) )
view . addGestureRecognizer ( tapGestureRecognizer )
// L i s t e n t o k e y b o a r d n o t i f i c a t i o n s
let notificationCenter = NotificationCenter . default
notificationCenter . addObserver ( self , selector : #selector ( handleKeyboardWillChangeFrameNotification ( _ : ) ) , name : UIResponder . keyboardWillChangeFrameNotification , object : nil )
notificationCenter . addObserver ( self , selector : #selector ( handleKeyboardWillHideNotification ( _ : ) ) , name : UIResponder . keyboardWillHideNotification , object : nil )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
// MARK: G e n e r a l
func constrainHeight ( to height : CGFloat ) {
view . set ( . height , to : height )
}
@objc private func dismissKeyboard ( ) {
publicKeyTextView . resignFirstResponder ( )
}
@objc private func enableCopyButton ( ) {
copyButton . isUserInteractionEnabled = true
UIView . transition ( with : copyButton , duration : 0.25 , options : . transitionCrossDissolve , animations : {
self . copyButton . setTitle ( NSLocalizedString ( " copy " , comment : " " ) , for : UIControl . State . normal )
} , completion : nil )
}
// MARK: U p d a t i n g
@objc private func handleKeyboardWillChangeFrameNotification ( _ notification : Notification ) {
guard ! isKeyboardShowing else { return }
isKeyboardShowing = true
guard let newHeight = ( notification . userInfo ? [ UIResponder . keyboardFrameEndUserInfoKey ] as ? NSValue ) ? . cgRectValue . size . height else { return }
bottomConstraint . constant = newHeight
UIView . animate ( withDuration : 0.25 ) {
[ self . spacer1 , self . separator , self . spacer2 , self . userPublicKeyLabel , self . spacer3 , self . buttonContainer ] . forEach {
$0 . alpha = 0
$0 . isHidden = true
}
self . view . layoutIfNeeded ( )
}
}
@objc private func handleKeyboardWillHideNotification ( _ notification : Notification ) {
guard isKeyboardShowing else { return }
isKeyboardShowing = false
bottomConstraint . constant = 0
UIView . animate ( withDuration : 0.25 ) {
[ self . spacer1 , self . separator , self . spacer2 , self . userPublicKeyLabel , self . spacer3 , self . buttonContainer ] . forEach {
$0 . alpha = 1
$0 . isHidden = false
}
self . view . layoutIfNeeded ( )
}
}
// MARK: I n t e r a c t i o n
@objc private func copyPublicKey ( ) {
UIPasteboard . general . string = getUserHexEncodedPublicKey ( )
copyButton . isUserInteractionEnabled = false
UIView . transition ( with : copyButton , duration : 0.25 , options : . transitionCrossDissolve , animations : {
self . copyButton . setTitle ( " Copied " , for : UIControl . State . normal )
} , completion : nil )
Timer . scheduledTimer ( timeInterval : 4 , target : self , selector : #selector ( enableCopyButton ) , userInfo : nil , repeats : false )
}
@objc private func sharePublicKey ( ) {
let shareVC = UIActivityViewController ( activityItems : [ getUserHexEncodedPublicKey ( ) ] , applicationActivities : nil )
newPrivateChatVC . navigationController ! . present ( shareVC , animated : true , completion : nil )
}
@objc fileprivate func startNewPrivateChatIfPossible ( ) {
let publicKey = publicKeyTextView . text ? . trimmingCharacters ( in : . whitespaces ) ? ? " "
newPrivateChatVC . startNewPrivateChatIfPossible ( with : publicKey )
}
}
private final class ScanQRCodePlaceholderVC : UIViewController {
weak var newPrivateChatVC : NewPrivateChatVC !
override func viewDidLoad ( ) {
// R e m o v e b a c k g r o u n d c o l o r
view . backgroundColor = . clear
// S e t u p e x p l a n a t i o n l a b e l
let explanationLabel = UILabel ( )
explanationLabel . textColor = Colors . text
explanationLabel . font = . systemFont ( ofSize : Values . smallFontSize )
explanationLabel . text = NSLocalizedString ( " vc_scan_qr_code_camera_access_explanation " , comment : " " )
explanationLabel . numberOfLines = 0
explanationLabel . textAlignment = . center
explanationLabel . lineBreakMode = . byWordWrapping
// S e t u p c a l l t o a c t i o n b u t t o n
let callToActionButton = UIButton ( )
callToActionButton . titleLabel ! . font = . boldSystemFont ( ofSize : Values . mediumFontSize )
callToActionButton . setTitleColor ( Colors . accent , for : UIControl . State . normal )
callToActionButton . setTitle ( NSLocalizedString ( " vc_scan_qr_code_grant_camera_access_button_title " , comment : " " ) , for : UIControl . State . normal )
callToActionButton . addTarget ( self , action : #selector ( requestCameraAccess ) , for : UIControl . Event . touchUpInside )
// S e t u p s t a c k v i e w
let stackView = UIStackView ( arrangedSubviews : [ explanationLabel , callToActionButton ] )
stackView . axis = . vertical
stackView . spacing = Values . mediumSpacing
stackView . alignment = . center
// S e t u p c o n s t r a i n t s
view . set ( . width , to : UIScreen . main . bounds . width )
view . addSubview ( stackView )
stackView . pin ( . leading , to : . leading , of : view , withInset : Values . massiveSpacing )
view . pin ( . trailing , to : . trailing , of : stackView , withInset : Values . massiveSpacing )
let verticalCenteringConstraint = stackView . center ( . vertical , in : view )
verticalCenteringConstraint . constant = - 16 // M a k e s t h i n g s a p p e a r c e n t e r e d v i s u a l l y
}
func constrainHeight ( to height : CGFloat ) {
view . set ( . height , to : height )
}
@objc private func requestCameraAccess ( ) {
ows_ask ( forCameraPermissions : { [ weak self ] hasCameraAccess in
if hasCameraAccess {
self ? . newPrivateChatVC . handleCameraAccessGranted ( )
} else {
// D o n o t h i n g
}
} )
}
}