Implement modal redesign

This commit is contained in:
Niels Andriesse 2019-12-05 11:42:31 +11:00
parent 13d79096aa
commit c279eb1be4
14 changed files with 221 additions and 193 deletions

View File

@ -2709,9 +2709,7 @@
B80C6B562384A56D00FDBC8B /* DeviceLinksVC.swift */,
B80C6B582384C4E700FDBC8B /* DeviceNameModal.swift */,
B80C6B5A2384C7F900FDBC8B /* DeviceNameModalDelegate.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
B86BD08023399883000F5AE3 /* QRCodeModal.swift */,
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -2729,7 +2727,6 @@
isa = PBXGroup;
children = (
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2782,8 +2779,11 @@
children = (
B8B5BCEB2394D869003823C9 /* Button.swift */,
B8BB82AA238F669C00BA5194 /* ConversationCell.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
B8BB82AC238F734800BA5194 /* ProfilePictureView.swift */,
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
B8BB82B82394911B00BA5194 /* Separator.swift */,
B8CCF638239721E20091D419 /* TabBar.swift */,
B8BB82B423947F2D00BA5194 /* TextField.swift */,

View File

@ -0,0 +1,79 @@
@objc(LKModal)
internal class Modal : UIViewController {
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
// MARK: Settings
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
// MARK: Components
lazy var contentView: UIView = {
let result = UIView()
result.backgroundColor = Colors.modalBackground
result.layer.cornerRadius = Values.modalCornerRadius
result.layer.masksToBounds = false
result.layer.borderColor = Colors.modalBorder.cgColor
result.layer.borderWidth = Values.borderThickness
result.layer.shadowColor = UIColor.black.cgColor
result.layer.shadowRadius = 8
result.layer.shadowOpacity = 0.64
return result
}()
lazy var cancelButton: UIButton = {
let result = UIButton()
result.set(.height, to: Values.mediumButtonHeight)
result.layer.cornerRadius = Values.modalButtonCornerRadius
result.backgroundColor = Colors.buttonBackground
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
result.setTitleColor(Colors.text, for: UIControl.State.normal)
result.setTitle(NSLocalizedString("Cancel", comment: ""), for: UIControl.State.normal)
return result
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(Values.modalBackgroundOpacity)
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
view.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Values.veryLargeSpacing).isActive = true
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: Values.veryLargeSpacing).isActive = true
verticalCenteringConstraint = contentView.center(.vertical, in: view)
populateContentView()
}
/// To be overridden by subclasses.
func populateContentView() {
preconditionFailure("populateContentView() is abstract and must be overridden.")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
verticalCenteringConstraint.constant = contentView.height() / 2 + view.height() / 2
view.layoutIfNeeded()
verticalCenteringConstraint.constant = 0
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
// MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: view)
if contentView.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
cancel()
}
}
@objc func cancel() {
dismiss(animated: true, completion: nil)
}
}

View File

@ -0,0 +1,53 @@
@objc(LKNukeDataModal)
final class NukeDataModal : Modal {
// MARK: Lifecycle
override func populateContentView() {
// Set up title label
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Clear All Data", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("This will delete your entire account, including all data, any messages currently linked to your public key, as well as your personal key pair.", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up nuke data button
let nukeDataButton = UIButton()
nukeDataButton.set(.height, to: Values.mediumButtonHeight)
nukeDataButton.layer.cornerRadius = Values.modalButtonCornerRadius
nukeDataButton.backgroundColor = Colors.destructive
nukeDataButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
nukeDataButton.setTitleColor(Colors.text, for: UIControl.State.normal)
nukeDataButton.setTitle(NSLocalizedString("Delete", comment: ""), for: UIControl.State.normal)
nukeDataButton.addTarget(self, action: #selector(nuke), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, nukeDataButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, buttonStackView ])
stackView.axis = .vertical
stackView.spacing = Values.largeSpacing
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
}
// MARK: Interaction
@objc private func nuke() {
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
}
}

View File

@ -0,0 +1,71 @@
@objc(LKSeedModal)
final class SeedModal : Modal {
private let mnemonic: String = {
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
if hexEncodedSeed == nil {
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
}
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}()
// MARK: Lifecycle
override func populateContentView() {
// Set up title label
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("Your Seed", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
// Set up mnemonic label
let mnemonicLabel = UILabel()
mnemonicLabel.textColor = Colors.text
mnemonicLabel.font = .systemFont(ofSize: Values.smallFontSize)
mnemonicLabel.text = mnemonic
mnemonicLabel.numberOfLines = 0
mnemonicLabel.lineBreakMode = .byWordWrapping
mnemonicLabel.textAlignment = .center
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("This is your personal password. It can be used to restore your account or migrate your account to a new device.", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
// Set up copy button
let copyButton = UIButton()
copyButton.set(.height, to: Values.mediumButtonHeight)
copyButton.layer.cornerRadius = Values.modalButtonCornerRadius
copyButton.backgroundColor = Colors.buttonBackground
copyButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
copyButton.setTitleColor(Colors.text, for: UIControl.State.normal)
copyButton.setTitle(NSLocalizedString("Copy", comment: ""), for: UIControl.State.normal)
copyButton.addTarget(self, action: #selector(copySeed), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, copyButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ titleLabel, mnemonicLabel, explanationLabel, buttonStackView ])
stackView.axis = .vertical
stackView.spacing = Values.largeSpacing
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
}
// MARK: Interaction
@objc private func copySeed() {
UIPasteboard.general.string = mnemonic
dismiss(animated: true, completion: nil)
}
}

View File

@ -25,6 +25,8 @@ final class Colors : NSObject {
@objc static let newConversationButtonShadow = UIColor(hex: 0x077C44)
@objc static let separator = UIColor(hex: 0x36383C)
@objc static let unimportantButtonBackground = UIColor(hex: 0x323232)
@objc static let settingButtonBackground = UIColor(hex: 0x1B1B1B)
@objc static let buttonBackground = UIColor(hex: 0x1B1B1B)
@objc static let settingButtonSelected = UIColor(hex: 0x0C0C0C)
@objc static let modalBackground = UIColor(hex: 0x101011)
@objc static let modalBorder = UIColor(hex: 0x212121)
}

View File

@ -6,6 +6,7 @@ final class Values : NSObject {
@objc static let unimportantElementOpacity = CGFloat(0.6)
@objc static let conversationCellTimestampOpacity = CGFloat(0.4)
@objc static let textFieldBorderOpacity = CGFloat(0.4)
@objc static let modalBackgroundOpacity = CGFloat(0.75)
// MARK: - Font Sizes
@objc static let smallFontSize = CGFloat(13)
@ -32,6 +33,8 @@ final class Values : NSObject {
@objc static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale }
@objc static let tabBarHeight = CGFloat(48)
@objc static let settingsButtonHeight = CGFloat(75)
@objc static let modalCornerRadius = CGFloat(10)
@objc static let modalButtonCornerRadius = CGFloat(5)
// MARK: - Distances
@objc static let smallSpacing = CGFloat(8)

View File

@ -137,7 +137,7 @@ final class SettingsVC : UIViewController {
UIGraphicsEndImageContext()
return image!
}
button.setBackgroundImage(getImage(withColor: Colors.settingButtonBackground), for: UIControl.State.normal)
button.setBackgroundImage(getImage(withColor: Colors.buttonBackground), for: UIControl.State.normal)
button.setBackgroundImage(getImage(withColor: Colors.settingButtonSelected), for: UIControl.State.highlighted)
button.addTarget(self, action: selector, for: UIControl.Event.touchUpInside)
button.set(.height, to: Values.settingsButtonHeight)
@ -209,12 +209,14 @@ final class SettingsVC : UIViewController {
@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)
}
}

View File

@ -137,7 +137,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey.removing05PrefixIfNeeded()
mnemonicLabel.text = Mnemonic.hash(hexEncodedString: hexEncodedPublicKey)
}
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
let buttonHeight = cancelButton.titleLabel!.font.pointSize * 48 / 17
authorizeButton.set(.height, to: buttonHeight)
cancelButton.set(.height, to: buttonHeight)
authorizeButton.isHidden = true

View File

@ -47,7 +47,7 @@ final class DeviceNameModal : Modal {
let buttonStackView = UIStackView(arrangedSubviews: [ okButton, cancelButton ])
buttonStackView.axis = .horizontal
buttonStackView.distribution = .fillEqually
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
let buttonHeight = cancelButton.titleLabel!.font.pointSize * 48 / 17
okButton.set(.height, to: buttonHeight)
cancelButton.set(.height, to: buttonHeight)
// Stack view

View File

@ -1,54 +0,0 @@
@objc(LKNukeDataModal)
final class NukeDataModal : Modal {
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
Analytics.shared.track("Nuke Data Modal Shown")
}
override func populateContentView() {
// Label
let titleLabel = UILabel()
titleLabel.textColor = Theme.primaryColor
titleLabel.font = UIFont.ows_dynamicTypeHeadlineClamped
titleLabel.text = NSLocalizedString("Clear All Data", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
// Explanation label
let explanationLabel = UILabel()
explanationLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
explanationLabel.text = NSLocalizedString("Are you sure you want to clear all your data? This will delete your entire account, including all conversations and your personal key pair.", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textColor = UIColor.ows_white
// Button stack view
let nukeButton = OWSFlatButton.button(title: NSLocalizedString("OK", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(nuke))
nukeButton.setBackgroundColors(upColor: .clear, downColor: .clear)
let buttonStackView = UIStackView(arrangedSubviews: [ nukeButton, cancelButton ])
buttonStackView.axis = .horizontal
buttonStackView.distribution = .fillEqually
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
nukeButton.set(.height, to: buttonHeight)
cancelButton.set(.height, to: buttonHeight)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), titleLabel, explanationLabel, buttonStackView ])
stackView.axis = .vertical
stackView.spacing = 16
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
}
// MARK: Interaction
@objc private func nuke() {
Analytics.shared.track("Data Nuked")
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
}
}

View File

@ -27,7 +27,7 @@ final class QRCodeModal : Modal {
let qrCode = UIImage(ciImage: scaledQRCodeAsCIImage)
imageView.image = qrCode
// Cancel button
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
let buttonHeight = cancelButton.titleLabel!.font.pointSize * 48 / 17
cancelButton.set(.height, to: buttonHeight)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), label, UIView.spacer(withHeight: 2), imageView, cancelButton ])

View File

@ -1,73 +0,0 @@
@objc(LKSeedModal)
final class SeedModal : Modal {
private let mnemonic: String = {
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
if hexEncodedSeed == nil {
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
}
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
Analytics.shared.track("Seed Modal Shown")
}
override func populateContentView() {
// Label
let titleLabel = UILabel()
titleLabel.textColor = Theme.primaryColor
titleLabel.font = UIFont.ows_dynamicTypeHeadlineClamped
titleLabel.text = NSLocalizedString("Your Seed", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
// Subtitle label
let subtitleLabel = UILabel()
subtitleLabel.textColor = Theme.primaryColor
subtitleLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
subtitleLabel.text = NSLocalizedString("This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device.", comment: "")
subtitleLabel.numberOfLines = 0
subtitleLabel.lineBreakMode = .byWordWrapping
subtitleLabel.textAlignment = .center
// Mnemonic label
let mnemonicLabel = UILabel()
let font = UIFont.ows_dynamicTypeCaption1Clamped
mnemonicLabel.font = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitItalic)!, size: font.pointSize)
mnemonicLabel.text = mnemonic
mnemonicLabel.numberOfLines = 0
mnemonicLabel.textAlignment = .center
mnemonicLabel.lineBreakMode = .byWordWrapping
mnemonicLabel.textColor = UIColor.ows_white
mnemonicLabel.alpha = 0.8
// Button stack view
let copyButton = OWSFlatButton.button(title: NSLocalizedString("Copy", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(copySeed))
copyButton.setBackgroundColors(upColor: .clear, downColor: .clear)
let buttonStackView = UIStackView(arrangedSubviews: [ copyButton, cancelButton ])
buttonStackView.axis = .horizontal
buttonStackView.distribution = .fillEqually
let buttonHeight = cancelButton.button.titleLabel!.font.pointSize * 48 / 17
copyButton.set(.height, to: buttonHeight)
cancelButton.set(.height, to: buttonHeight)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ UIView.spacer(withHeight: 2), titleLabel, subtitleLabel, mnemonicLabel, buttonStackView ])
stackView.axis = .vertical
stackView.spacing = 16
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
stackView.pin(.top, to: .top, of: contentView, withInset: 16)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 16)
}
// MARK: Interaction
@objc private func copySeed() {
UIPasteboard.general.string = mnemonic
dismiss(animated: true, completion: nil)
}
}

View File

@ -1,58 +0,0 @@
@objc(LKModal)
internal class Modal : UIViewController {
private(set) var verticalCenteringConstraint: NSLayoutConstraint!
// MARK: Components
lazy var contentView: UIView = {
let result = UIView()
result.backgroundColor = .lokiDarkGray()
result.layer.cornerRadius = 4
result.layer.masksToBounds = false
result.layer.shadowColor = UIColor.black.cgColor
result.layer.shadowRadius = 8
result.layer.shadowOpacity = 0.64
return result
}()
lazy var cancelButton: OWSFlatButton = {
let result = OWSFlatButton.button(title: NSLocalizedString("Cancel", comment: ""), font: .ows_dynamicTypeBodyClamped, titleColor: .white, backgroundColor: .clear, target: self, selector: #selector(cancel))
result.setBackgroundColors(upColor: .clear, downColor: .clear)
return result
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
view.backgroundColor = .clear
view.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 32).isActive = true
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 32).isActive = true
verticalCenteringConstraint = contentView.center(.vertical, in: view)
populateContentView()
}
/// To be overridden by subclasses.
func populateContentView() {
preconditionFailure("populateContentView() is abstract and must be overridden.")
}
// MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: view)
if contentView.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
cancel()
}
}
@objc func cancel() {
dismiss(animated: true, completion: nil)
}
}

View File

@ -2705,3 +2705,6 @@
"Linked Devices" = "Linked Devices";
"Show Seed" = "Show Seed";
"Clear All Data" = "Clear All Data";
"This will delete your entire account, including all data, any messages currently linked to your public key, as well as your personal key pair." = "This will delete your entire account, including all data, any messages currently linked to your public key, as well as your personal key pair.";
"Delete" = "Delete";
"This is your personal password. It can be used to restore your account or migrate your account to a new device." = "This is your personal password. It can be used to restore your account or migrate your account to a new device.";