final class SettingsVC : BaseVC , AvatarViewHelperDelegate {
private var profilePictureToBeUploaded : UIImage ?
private var displayNameToBeUploaded : String ?
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged ( ) } }
// MARK: C o m p o n e n t s
private lazy var profilePictureView : ProfilePictureView = {
let result = ProfilePictureView ( )
let size = Values . largeProfilePictureSize
result . size = size
result . set ( . width , to : size )
result . set ( . height , to : size )
result . accessibilityLabel = " Edit profile picture button "
result . isAccessibilityElement = true
return result
} ( )
private lazy var profilePictureUtilities : AvatarViewHelper = {
let result = AvatarViewHelper ( )
result . delegate = self
return result
} ( )
private lazy var displayNameLabel : UILabel = {
let result = UILabel ( )
result . textColor = Colors . text
result . font = . boldSystemFont ( ofSize : Values . veryLargeFontSize )
result . lineBreakMode = . byTruncatingTail
result . textAlignment = . center
return result
} ( )
private lazy var displayNameTextField : TextField = {
let result = TextField ( placeholder : NSLocalizedString ( " vc_settings_display_name_text_field_hint " , comment : " " ) , usesDefaultHeight : false )
result . textAlignment = . center
result . accessibilityLabel = " Edit display name text field "
return result
} ( )
private lazy var copyButton : Button = {
let result = Button ( style : . prominentOutline , 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 settingButtonsStackView : UIStackView = {
let result = UIStackView ( )
result . axis = . vertical
result . alignment = . fill
return result
} ( )
// MARK: S e t t i n g s
private static let buttonHeight = isIPhone5OrSmaller ? CGFloat ( 52 ) : CGFloat ( 75 )
// MARK: L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
setUpGradientBackground ( )
setUpNavBarStyle ( )
setNavBarTitle ( NSLocalizedString ( " vc_settings_title " , comment : " " ) )
// 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 backButton = UIBarButtonItem ( title : " Back " , style : . plain , target : nil , action : nil )
backButton . tintColor = Colors . text
navigationItem . backBarButtonItem = backButton
updateNavigationBarButtons ( )
// S e t u p p r o f i l e p i c t u r e v i e w
let profilePictureTapGestureRecognizer = UITapGestureRecognizer ( target : self , action : #selector ( showEditProfilePictureUI ) )
profilePictureView . addGestureRecognizer ( profilePictureTapGestureRecognizer )
profilePictureView . hexEncodedPublicKey = getUserHexEncodedPublicKey ( )
profilePictureView . update ( )
// S e t u p d i s p l a y n a m e l a b e l
displayNameLabel . text = OWSProfileManager . shared ( ) . profileNameForRecipient ( withID : getUserHexEncodedPublicKey ( ) )
// S e t u p d i s p l a y n a m e c o n t a i n e r
let displayNameContainer = UIView ( )
displayNameContainer . accessibilityLabel = " Edit display name text field "
displayNameContainer . isAccessibilityElement = true
displayNameContainer . addSubview ( displayNameLabel )
displayNameLabel . pin ( to : displayNameContainer )
displayNameContainer . addSubview ( displayNameTextField )
displayNameTextField . pin ( to : displayNameContainer )
displayNameContainer . set ( . height , to : 40 )
displayNameTextField . alpha = 0
let displayNameContainerTapGestureRecognizer = UITapGestureRecognizer ( target : self , action : #selector ( showEditDisplayNameUI ) )
displayNameContainer . addGestureRecognizer ( displayNameContainerTapGestureRecognizer )
// S e t u p h e a d e r v i e w
let headerStackView = UIStackView ( arrangedSubviews : [ profilePictureView , displayNameContainer ] )
headerStackView . axis = . vertical
headerStackView . spacing = Values . smallSpacing
headerStackView . alignment = . center
// S e t u p s e p a r a t o r
let separator = Separator ( title : NSLocalizedString ( " your_session_id " , comment : " " ) )
// S e t u p p u b l i c k e y l a b e l
let publicKeyLabel = UILabel ( )
publicKeyLabel . textColor = Colors . text
publicKeyLabel . font = Fonts . spaceMono ( ofSize : isIPhone5OrSmaller ? Values . mediumFontSize : Values . largeFontSize )
publicKeyLabel . numberOfLines = 0
publicKeyLabel . textAlignment = . center
publicKeyLabel . lineBreakMode = . byCharWrapping
publicKeyLabel . text = getUserHexEncodedPublicKey ( )
// S e t u p s h a r e b u t t o n
let shareButton = Button ( style : . regular , size : . medium )
shareButton . setTitle ( NSLocalizedString ( " share " , comment : " " ) , for : UIControl . State . normal )
shareButton . addTarget ( self , action : #selector ( sharePublicKey ) , for : UIControl . Event . touchUpInside )
// S e t u p b u t t o n c o n t a i n e r
let buttonContainer = UIStackView ( arrangedSubviews : [ copyButton , shareButton ] )
buttonContainer . axis = . horizontal
buttonContainer . spacing = Values . mediumSpacing
buttonContainer . distribution = . fillEqually
// S e t u p t o p s t a c k v i e w
let topStackView = UIStackView ( arrangedSubviews : [ headerStackView , separator , publicKeyLabel , buttonContainer ] )
topStackView . axis = . vertical
topStackView . spacing = Values . largeSpacing
topStackView . alignment = . fill
topStackView . layoutMargins = UIEdgeInsets ( top : 0 , left : Values . largeSpacing , bottom : 0 , right : Values . largeSpacing )
topStackView . isLayoutMarginsRelativeArrangement = true
// S e t u p s e t t i n g b u t t o n s s t a c k v i e w
getSettingButtons ( ) . forEach { settingButtonOrSeparator in
settingButtonsStackView . addArrangedSubview ( settingButtonOrSeparator )
}
// S e t u p v e r s i o n l a b e l
let versionLabel = UILabel ( )
versionLabel . textColor = Colors . text . withAlphaComponent ( Values . mediumOpacity )
versionLabel . font = . systemFont ( ofSize : Values . verySmallFontSize )
versionLabel . numberOfLines = 0
versionLabel . textAlignment = . center
versionLabel . lineBreakMode = . byCharWrapping
let version = Bundle . main . infoDictionary ! [ " CFBundleShortVersionString " ] !
let buildNumber = Bundle . main . infoDictionary ! [ " CFBundleVersion " ] !
versionLabel . text = " Version \( version ) ( \( buildNumber ) ) "
// S e t u p s t a c k v i e w
let stackView = UIStackView ( arrangedSubviews : [ topStackView , settingButtonsStackView , versionLabel ] )
stackView . axis = . vertical
stackView . spacing = Values . largeSpacing
stackView . alignment = . fill
stackView . layoutMargins = UIEdgeInsets ( top : Values . mediumSpacing , left : 0 , bottom : Values . mediumSpacing , right : 0 )
stackView . isLayoutMarginsRelativeArrangement = true
stackView . set ( . width , to : UIScreen . main . bounds . width )
// S e t u p s c r o l l v i e w
let scrollView = UIScrollView ( )
scrollView . showsVerticalScrollIndicator = false
scrollView . addSubview ( stackView )
stackView . pin ( to : scrollView )
view . addSubview ( scrollView )
scrollView . pin ( to : view )
}
private func getSettingButtons ( ) -> [ UIView ] {
func getSeparator ( ) -> UIView {
let result = UIView ( )
result . backgroundColor = Colors . separator
result . set ( . height , to : Values . separatorThickness )
return result
}
func getSettingButton ( withTitle title : String , color : UIColor , action selector : Selector ) -> UIButton {
let button = UIButton ( )
button . setTitle ( title , for : UIControl . State . normal )
button . setTitleColor ( color , for : UIControl . State . normal )
button . titleLabel ! . font = . boldSystemFont ( ofSize : Values . mediumFontSize )
button . titleLabel ! . textAlignment = . center
func getImage ( withColor color : UIColor ) -> UIImage {
let rect = CGRect ( origin : CGPoint . zero , size : CGSize ( width : 1 , height : 1 ) )
UIGraphicsBeginImageContext ( rect . size )
let context = UIGraphicsGetCurrentContext ( ) !
context . setFillColor ( color . cgColor )
context . fill ( rect )
let image = UIGraphicsGetImageFromCurrentImageContext ( )
UIGraphicsEndImageContext ( )
return image !
}
let backgroundColor = isLightMode ? UIColor ( hex : 0xFCFCFC ) : UIColor ( hex : 0x1B1B1B )
button . setBackgroundImage ( getImage ( withColor : backgroundColor ) , for : UIControl . State . normal )
let selectedColor = isLightMode ? UIColor ( hex : 0xDFDFDF ) : UIColor ( hex : 0x0C0C0C )
button . setBackgroundImage ( getImage ( withColor : selectedColor ) , for : UIControl . State . highlighted )
button . addTarget ( self , action : selector , for : UIControl . Event . touchUpInside )
button . set ( . height , to : SettingsVC . buttonHeight )
return button
}
var result = [
getSeparator ( ) ,
getSettingButton ( withTitle : NSLocalizedString ( " vc_settings_privacy_button_title " , comment : " " ) , color : Colors . text , action : #selector ( showPrivacySettings ) ) ,
getSeparator ( ) ,
getSettingButton ( withTitle : NSLocalizedString ( " vc_settings_notifications_button_title " , comment : " " ) , color : Colors . text , action : #selector ( showNotificationSettings ) ) ,
getSeparator ( ) ,
getSettingButton ( withTitle : " Invite " , color : Colors . text , action : #selector ( sendInvitation ) ) ,
getSeparator ( )
]
if ! KeyPairUtilities . hasV2KeyPair ( ) {
result += [
getSettingButton ( withTitle : " Upgrade Session ID " , color : Colors . text , action : #selector ( upgradeSessionID ) ) ,
getSeparator ( )
]
}
result += [
getSettingButton ( withTitle : NSLocalizedString ( " vc_settings_recovery_phrase_button_title " , comment : " " ) , color : Colors . text , action : #selector ( showSeed ) ) ,
getSeparator ( ) ,
getSettingButton ( withTitle : NSLocalizedString ( " vc_settings_clear_all_data_button_title " , comment : " " ) , color : Colors . destructive , action : #selector ( clearAllData ) ) ,
getSeparator ( )
]
return result
}
// MARK: G e n e r a l
@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 )
}
func avatarActionSheetTitle ( ) -> String ? {
return " Update Profile Picture "
}
func fromViewController ( ) -> UIViewController {
return self
}
func hasClearAvatarAction ( ) -> Bool {
return false
}
func clearAvatarActionLabel ( ) -> String {
return " Clear "
}
// MARK: U p d a t i n g
private func handleIsEditingDisplayNameChanged ( ) {
updateNavigationBarButtons ( )
UIView . animate ( withDuration : 0.25 ) {
self . displayNameLabel . alpha = self . isEditingDisplayName ? 0 : 1
self . displayNameTextField . alpha = self . isEditingDisplayName ? 1 : 0
}
if isEditingDisplayName {
displayNameTextField . becomeFirstResponder ( )
} else {
displayNameTextField . resignFirstResponder ( )
}
}
private func updateNavigationBarButtons ( ) {
if isEditingDisplayName {
let cancelButton = UIBarButtonItem ( barButtonSystemItem : . cancel , target : self , action : #selector ( handleCancelDisplayNameEditingButtonTapped ) )
cancelButton . tintColor = Colors . text
cancelButton . accessibilityLabel = " Cancel button "
cancelButton . isAccessibilityElement = true
navigationItem . leftBarButtonItem = cancelButton
let doneButton = UIBarButtonItem ( barButtonSystemItem : . done , target : self , action : #selector ( handleSaveDisplayNameButtonTapped ) )
doneButton . tintColor = Colors . text
doneButton . accessibilityLabel = " Done button "
doneButton . isAccessibilityElement = true
navigationItem . rightBarButtonItem = doneButton
} else {
let closeButton = UIBarButtonItem ( image : # imageLiteral ( resourceName : " X " ) , style : . plain , target : self , action : #selector ( close ) )
closeButton . tintColor = Colors . text
closeButton . accessibilityLabel = " Close button "
closeButton . isAccessibilityElement = true
navigationItem . leftBarButtonItem = closeButton
if #available ( iOS 13 , * ) { // P r e i O S 1 3 t h e u s e r c a n ' t s w i t c h a c t i v e l y b u t t h e a p p s t i l l r e s p o n d s t o s y s t e m c h a n g e s
let appModeIcon = isDarkMode ? # imageLiteral ( resourceName : " ic_dark_theme_on " ) . withTintColor ( . white ) : # imageLiteral ( resourceName : " ic_dark_theme_off " ) . withTintColor ( . black )
let appModeButton = UIButton ( )
appModeButton . setImage ( appModeIcon , for : UIControl . State . normal )
appModeButton . tintColor = Colors . text
appModeButton . addTarget ( self , action : #selector ( switchAppMode ) , for : UIControl . Event . touchUpInside )
appModeButton . accessibilityLabel = " Switch app mode button "
let qrCodeIcon = isDarkMode ? # imageLiteral ( resourceName : " QRCode " ) . withTintColor ( . white ) : # imageLiteral ( resourceName : " QRCode " ) . withTintColor ( . black )
let qrCodeButton = UIButton ( )
qrCodeButton . setImage ( qrCodeIcon , for : UIControl . State . normal )
qrCodeButton . tintColor = Colors . text
qrCodeButton . addTarget ( self , action : #selector ( showQRCode ) , for : UIControl . Event . touchUpInside )
qrCodeButton . accessibilityLabel = " Show QR code button "
let stackView = UIStackView ( arrangedSubviews : [ appModeButton , qrCodeButton ] )
stackView . axis = . horizontal
stackView . spacing = Values . mediumSpacing
navigationItem . rightBarButtonItem = UIBarButtonItem ( customView : stackView )
} else {
let qrCodeIcon = isDarkMode ? # imageLiteral ( resourceName : " QRCode " ) . asTintedImage ( color : . white ) : # imageLiteral ( resourceName : " QRCode " ) . asTintedImage ( color : . black )
let qrCodeButton = UIBarButtonItem ( image : qrCodeIcon , style : . plain , target : self , action : #selector ( showQRCode ) )
qrCodeButton . tintColor = Colors . text
navigationItem . rightBarButtonItem = qrCodeButton
}
}
}
func avatarDidChange ( _ image : UIImage ) {
let maxSize = Int ( kOWSProfileManager_MaxAvatarDiameter )
profilePictureToBeUploaded = image . resizedImage ( toFillPixelSize : CGSize ( width : maxSize , height : maxSize ) )
updateProfile ( isUpdatingDisplayName : false , isUpdatingProfilePicture : true )
}
func clearAvatar ( ) {
profilePictureToBeUploaded = nil
updateProfile ( isUpdatingDisplayName : false , isUpdatingProfilePicture : true )
}
private func updateProfile ( isUpdatingDisplayName : Bool , isUpdatingProfilePicture : Bool ) {
let userDefaults = UserDefaults . standard
let displayName = displayNameToBeUploaded ? ? OWSProfileManager . shared ( ) . profileNameForRecipient ( withID : getUserHexEncodedPublicKey ( ) )
let profilePicture = profilePictureToBeUploaded ? ? OWSProfileManager . shared ( ) . profileAvatar ( forRecipientId : getUserHexEncodedPublicKey ( ) )
ModalActivityIndicatorViewController . present ( fromViewController : navigationController ! , canCancel : false ) { [ weak self , displayNameToBeUploaded , profilePictureToBeUploaded ] modalActivityIndicator in
OWSProfileManager . shared ( ) . updateLocalProfileName ( displayName , avatarImage : profilePicture , success : {
if displayNameToBeUploaded != nil {
userDefaults [ . lastDisplayNameUpdate ] = Date ( )
}
if profilePictureToBeUploaded != nil {
userDefaults [ . lastProfilePictureUpdate ] = Date ( )
}
let appDelegate = UIApplication . shared . delegate as ! AppDelegate
appDelegate . forceSyncConfigurationNowIfNeeded ( ) . retainUntilComplete ( )
DispatchQueue . main . async {
modalActivityIndicator . dismiss {
guard let self = self else { return }
self . profilePictureView . update ( )
self . displayNameLabel . text = displayName
self . profilePictureToBeUploaded = nil
self . displayNameToBeUploaded = nil
}
}
} , failure : { error in
DispatchQueue . main . async {
modalActivityIndicator . dismiss {
var isMaxFileSizeExceeded = false
if let error = error as ? DotNetAPI . Error {
isMaxFileSizeExceeded = ( error = = . maxFileSizeExceeded )
}
let title = isMaxFileSizeExceeded ? " Maximum File Size Exceeded " : " Couldn't Update Profile "
let message = isMaxFileSizeExceeded ? " Please select a smaller photo and try again " : " Please check your internet connection and try again "
let alert = UIAlertController ( title : title , message : message , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " OK " , comment : " " ) , style : . default , handler : nil ) )
self ? . present ( alert , animated : true , completion : nil )
}
}
} , requiresSync : true )
}
}
@objc override internal func handleAppModeChangedNotification ( _ notification : Notification ) {
super . handleAppModeChangedNotification ( notification )
updateNavigationBarButtons ( )
settingButtonsStackView . arrangedSubviews . forEach { settingButton in
settingButtonsStackView . removeArrangedSubview ( settingButton )
settingButton . removeFromSuperview ( )
}
getSettingButtons ( ) . forEach { settingButtonOrSeparator in
settingButtonsStackView . addArrangedSubview ( settingButtonOrSeparator ) // R e - d o t h e s e t t i n g b u t t o n s
}
}
// MARK: I n t e r a c t i o n
@objc private func close ( ) {
dismiss ( animated : true , completion : nil )
}
@objc private func switchAppMode ( ) {
let newAppMode : AppMode = isLightMode ? . dark : . light
AppModeManager . shared . setCurrentAppMode ( to : newAppMode )
}
@objc private func showQRCode ( ) {
let qrCodeVC = QRCodeVC ( )
navigationController ! . pushViewController ( qrCodeVC , animated : true )
}
@objc private func handleCancelDisplayNameEditingButtonTapped ( ) {
isEditingDisplayName = false
}
@objc private func handleSaveDisplayNameButtonTapped ( ) {
func showError ( title : String , message : String = " " ) {
let alert = UIAlertController ( title : title , message : message , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " OK " , comment : " " ) , style : . default , handler : nil ) )
presentAlert ( alert )
}
let displayName = displayNameTextField . text ! . trimmingCharacters ( in : CharacterSet . whitespacesAndNewlines )
guard ! displayName . isEmpty else {
return showError ( title : NSLocalizedString ( " vc_settings_display_name_missing_error " , comment : " " ) )
}
guard ! OWSProfileManager . shared ( ) . isProfileNameTooLong ( displayName ) else {
return showError ( title : NSLocalizedString ( " vc_settings_display_name_too_long_error " , comment : " " ) )
}
isEditingDisplayName = false
displayNameToBeUploaded = displayName
updateProfile ( isUpdatingDisplayName : true , isUpdatingProfilePicture : false )
}
@objc private func showEditProfilePictureUI ( ) {
profilePictureUtilities . showChangeAvatarUI ( )
}
@objc private func showEditDisplayNameUI ( ) {
isEditingDisplayName = true
}
@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 )
navigationController ! . present ( shareVC , animated : true , completion : nil )
}
@objc private func showPrivacySettings ( ) {
let privacySettingsVC = PrivacySettingsTableViewController ( )
navigationController ! . pushViewController ( privacySettingsVC , animated : true )
}
@objc private func showNotificationSettings ( ) {
let notificationSettingsVC = NotificationSettingsViewController ( )
navigationController ! . pushViewController ( notificationSettingsVC , animated : true )
}
@objc private func sendInvitation ( ) {
let invitation = " Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \( getUserHexEncodedPublicKey ( ) ) ! "
let shareVC = UIActivityViewController ( activityItems : [ invitation ] , applicationActivities : nil )
navigationController ! . present ( shareVC , animated : true , completion : nil )
}
@objc private func upgradeSessionID ( ) {
let message = " You’ re upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed? "
let alert = UIAlertController ( title : " Upgrade Session ID? " , message : message , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : " Yes " , style : . destructive ) { _ in
Storage . prepareForV2KeyPairMigration ( )
} )
alert . addAction ( UIAlertAction ( title : " Cancel " , style : . default , handler : nil ) )
present ( alert , animated : true , completion : nil )
}
@objc private func showSeed ( ) {
let seedModal = SeedModal ( )
seedModal . modalPresentationStyle = . overFullScreen
seedModal . modalTransitionStyle = . crossDissolve
present ( seedModal , animated : true , completion : nil )
}
@objc private func clearAllData ( ) {
let nukeDataModal = NukeDataModal ( )
nukeDataModal . modalPresentationStyle = . overFullScreen
nukeDataModal . modalTransitionStyle = . crossDissolve
present ( nukeDataModal , animated : true , completion : nil )
}
}