diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 19ac884b3..c54ae53bb 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -537,6 +537,9 @@ C3548F0624456447009433A8 /* PNModeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0524456447009433A8 /* PNModeVC.swift */; }; C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; + C35D0DA125AE582D00B6BF49 /* MultiDeviceVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DA025AE582D00B6BF49 /* MultiDeviceVC.swift */; }; + C35D0DAB25AE5BDE00B6BF49 /* SettingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DAA25AE5BDE00B6BF49 /* SettingRow.swift */; }; + C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */; }; C35E8AA82485C85800ACB629 /* GeoLite2-Country-Locations-English.csv in Resources */ = {isa = PBXBuildFile; fileRef = C35E8AA52485C85400ACB629 /* GeoLite2-Country-Locations-English.csv */; }; C35E8AA92485C85800ACB629 /* GeoLite2-Country-Blocks-IPv4.csv in Resources */ = {isa = PBXBuildFile; fileRef = C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */; }; C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35E8AAD2485E51D00ACB629 /* IP2Country.swift */; }; @@ -1537,6 +1540,9 @@ C3548F0524456447009433A8 /* PNModeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeVC.swift; sourceTree = ""; }; C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Wrapping.swift"; sourceTree = ""; }; C354E75923FE2A7600CE22E3 /* BaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVC.swift; sourceTree = ""; }; + C35D0DA025AE582D00B6BF49 /* MultiDeviceVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDeviceVC.swift; sourceTree = ""; }; + C35D0DAA25AE5BDE00B6BF49 /* SettingRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingRow.swift; sourceTree = ""; }; + C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = ""; }; C35E8AA22485C72300ACB629 /* SwiftCSV.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftCSV.framework; path = ThirdParty/Carthage/Build/iOS/SwiftCSV.framework; sourceTree = ""; }; C35E8AA52485C85400ACB629 /* GeoLite2-Country-Locations-English.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeoLite2-Country-Locations-English.csv"; sourceTree = ""; }; C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeoLite2-Country-Blocks-IPv4.csv"; sourceTree = ""; }; @@ -2271,6 +2277,7 @@ C33FDB3F255A580C00E217F9 /* String+SSK.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */, + C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, C38EF23D255B6D66007E1867 /* UIView+OWS.h */, C38EF23E255B6D66007E1867 /* UIView+OWS.m */, C38EF2EF255B6DBB007E1867 /* Weak.swift */, @@ -2319,6 +2326,7 @@ C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */, 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */, 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */, + C35D0DAA25AE5BDE00B6BF49 /* SettingRow.swift */, ); path = Shared; sourceTree = ""; @@ -2742,6 +2750,7 @@ B886B4A62398B23E00211ABE /* QRCodeVC.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, B8CCF6422397711F0091D419 /* SettingsVC.swift */, + C35D0DA025AE582D00B6BF49 /* MultiDeviceVC.swift */, ); path = Settings; sourceTree = ""; @@ -4812,6 +4821,7 @@ C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */, C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */, B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */, + C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */, C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, @@ -5037,6 +5047,7 @@ B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, D221A09A169C9E5E00537ABF /* main.m in Sources */, 3496957221A301A100DCFE74 /* OWSBackup.m in Sources */, + C35D0DA125AE582D00B6BF49 /* MultiDeviceVC.swift in Sources */, B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */, 34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */, 34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */, @@ -5045,6 +5056,7 @@ B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */, 346129991FD1E4DA00532771 /* SignalApp.m in Sources */, 3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */, + C35D0DAB25AE5BDE00B6BF49 /* SettingRow.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */, C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */, diff --git a/Session/Settings/MultiDeviceVC.swift b/Session/Settings/MultiDeviceVC.swift new file mode 100644 index 000000000..8efc9bca8 --- /dev/null +++ b/Session/Settings/MultiDeviceVC.swift @@ -0,0 +1,161 @@ + +final class MultiDeviceVC : BaseVC { + + private let mnemonic: String = { + let collection = OWSPrimaryStorageIdentityKeyStoreCollection + let hexEncodedSeed: String! = OWSIdentityManager.shared().dbConnection.object(forKey: "LKLokiSeed", inCollection: collection) as! String? + return Mnemonic.encode(hexEncodedString: hexEncodedSeed) + }() + + // MARK: UI Components + private lazy var toggleLabel: UILabel = { + let result = UILabel() + result.textColor = Colors.text + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.text = "Enable multi device" + return result + }() + + private lazy var toggle: UISwitch = { + let result = UISwitch() + result.onTintColor = Colors.accent + return result + }() + + private lazy var stepsRow: SettingRow = { + let result = SettingRow(autoSize: true) + result.isHidden = true + return result + }() + + private lazy var stepsLabel1: UILabel = { + let result = UILabel() + result.textColor = Colors.text + result.font = .systemFont(ofSize: Values.smallFontSize) + result.text = """ + 1. Clear your other device if it currently has an account on it (Settings > Clear Data). + + 2. On the landing page, click "Continue your Session". + + 3. Enter the following words when prompted: + """ + result.numberOfLines = 0 + result.lineBreakMode = .byWordWrapping + return result + }() + + private lazy var mnemonicLabel: UILabel = { + let result = UILabel() + result.textColor = Colors.text + result.font = Fonts.spaceMono(ofSize: Values.smallFontSize) + result.text = mnemonic + result.numberOfLines = 0 + result.lineBreakMode = .byWordWrapping + result.textAlignment = .center + 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(copyMnemonic), for: UIControl.Event.touchUpInside) + return result + }() + + private lazy var stepsLabel2: UILabel = { + let result = UILabel() + result.textColor = Colors.text + result.font = .systemFont(ofSize: Values.smallFontSize) + result.text = """ + 4. Enter your display name. + + 5. That's it! + """ + result.numberOfLines = 0 + result.lineBreakMode = .byWordWrapping + return result + }() + + // MARK: Initialization + override func viewDidLoad() { + super.viewDidLoad() + setUpUI() + } + + private func setUpUI() { + setUpGradientBackground() + setUpNavBarStyle() + setNavBarTitle("Multi Device (Beta)") + // Back button + let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + backButton.tintColor = Colors.text + navigationItem.backBarButtonItem = backButton + // Toggle + toggle.addTarget(self, action: #selector(handleToggle), for: UIControl.Event.valueChanged) + let toggleStackView = UIStackView(arrangedSubviews: [ toggleLabel, toggle ]) + toggleStackView.axis = .horizontal + toggleStackView.spacing = Values.mediumSpacing + toggleStackView.alignment = .center + let toggleRow = SettingRow() + toggleRow.contentView.addSubview(toggleStackView) + toggleStackView.pin(to: toggleRow.contentView, withInset: Values.mediumSpacing) + // Steps + let mnemonicLabelContainer = UIView() + mnemonicLabelContainer.addSubview(mnemonicLabel) + mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: isIPhone6OrSmaller ? 4 : Values.smallSpacing) + mnemonicLabelContainer.layer.cornerRadius = Values.textFieldCornerRadius + mnemonicLabelContainer.layer.borderWidth = Values.borderThickness + mnemonicLabelContainer.layer.borderColor = Colors.text.cgColor + let stepsLabel1Container = UIView() + stepsLabel1Container.addSubview(stepsLabel1) + stepsLabel1.pin(.leading, to: .leading, of: stepsLabel1Container, withInset: Values.smallSpacing) + stepsLabel1Container.pin(.trailing, to: .trailing, of: stepsLabel1, withInset: Values.smallSpacing) + stepsLabel1.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: stepsLabel1Container) + let stepsLabel2Container = UIView() + stepsLabel2Container.addSubview(stepsLabel2) + stepsLabel2.pin(.leading, to: .leading, of: stepsLabel2Container, withInset: Values.smallSpacing) + stepsLabel2Container.pin(.trailing, to: .trailing, of: stepsLabel2, withInset: Values.smallSpacing) + stepsLabel2.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: stepsLabel2Container) + let stepsStackView = UIStackView(arrangedSubviews: [ stepsLabel1Container, mnemonicLabelContainer, copyButton, stepsLabel2Container ]) + stepsStackView.axis = .vertical + stepsStackView.spacing = Values.mediumSpacing + stepsRow.contentView.addSubview(stepsStackView) + stepsStackView.pin(to: stepsRow.contentView, withInset: Values.mediumSpacing) + // Main stack view + let mainStackView = UIStackView(arrangedSubviews: [ toggleRow, stepsRow ]) + mainStackView.axis = .vertical + mainStackView.spacing = Values.mediumSpacing + mainStackView.isLayoutMarginsRelativeArrangement = true + mainStackView.layoutMargins = UIEdgeInsets(uniform: Values.mediumSpacing) + mainStackView.set(.width, to: UIScreen.main.bounds.width) + // Scroll view + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.addSubview(mainStackView) + mainStackView.pin(to: scrollView) + view.addSubview(scrollView) + scrollView.pin(to: view) + } + + // MARK: Updating + @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: Interaction + @objc private func handleToggle() { + stepsRow.isHidden = !toggle.isOn + } + + @objc private func copyMnemonic() { + UIPasteboard.general.string = mnemonic + 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) + } +} diff --git a/Session/Settings/SeedModal.swift b/Session/Settings/SeedModal.swift index 593a61d12..d5dd02e53 100644 --- a/Session/Settings/SeedModal.swift +++ b/Session/Settings/SeedModal.swift @@ -59,16 +59,8 @@ final class SeedModal : Modal { buttonStackView.axis = .horizontal buttonStackView.spacing = Values.mediumSpacing buttonStackView.distribution = .fillEqually - // Set up explanation label - let disclaimerLabel = UILabel() - disclaimerLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) - disclaimerLabel.font = .systemFont(ofSize: 10) - disclaimerLabel.text = "It is not possible to use the same Session ID on multiple devices simultaneously" - disclaimerLabel.numberOfLines = 0 - disclaimerLabel.lineBreakMode = .byWordWrapping - disclaimerLabel.textAlignment = .center // Set up stack view - let stackView = UIStackView(arrangedSubviews: [ titleLabel, mnemonicLabelContainer, explanationLabel, buttonStackView, disclaimerLabel ]) + let stackView = UIStackView(arrangedSubviews: [ titleLabel, mnemonicLabelContainer, explanationLabel, buttonStackView ]) stackView.axis = .vertical stackView.spacing = Values.largeSpacing contentView.addSubview(stackView) diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index c7cadabb7..1e80b1ad4 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -181,6 +181,8 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { getSeparator(), getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)), getSeparator(), + getSettingButton(withTitle: "Multi Device (Beta)", color: Colors.text, action: #selector(showMultiDeviceOptions)), + getSeparator(), getSettingButton(withTitle: "Invite", color: Colors.text, action: #selector(sendInvitation)), getSeparator() ] @@ -405,6 +407,11 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { navigationController!.pushViewController(notificationSettingsVC, animated: true) } + @objc private func showMultiDeviceOptions() { + let multiDeviceVC = MultiDeviceVC() + navigationController!.pushViewController(multiDeviceVC, 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) diff --git a/Session/Shared/SettingRow.swift b/Session/Shared/SettingRow.swift new file mode 100644 index 000000000..45283075a --- /dev/null +++ b/Session/Shared/SettingRow.swift @@ -0,0 +1,51 @@ + +final class SettingRow : UIView { + private let autoSize: Bool + + lazy var contentView: UIView = { + let result = UIView() + result.backgroundColor = Colors.buttonBackground + result.layer.cornerRadius = 8 + result.layer.masksToBounds = true + return result + }() + + init(autoSize: Bool) { + self.autoSize = autoSize + super.init(frame: CGRect.zero) + setUpUI() + } + + override init(frame: CGRect) { + autoSize = false + super.init(frame: frame) + setUpUI() + } + + required init?(coder: NSCoder) { + autoSize = false + super.init(coder: coder) + setUpUI() + } + + private func setUpUI() { + // Height + if !autoSize { + let height = Values.defaultSettingRowHeight + set(.height, to: height) + } + // Shadow + layer.shadowColor = UIColor.black.cgColor + layer.shadowOffset = CGSize.zero + layer.shadowOpacity = 0.4 + layer.shadowRadius = 4 + // Content view + addSubview(contentView) + contentView.pin(to: self) + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 8).cgPath + } +} diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index 59cf80756..fd680f4d2 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -39,6 +39,7 @@ public final class Values : NSObject { @objc public static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale } @objc public static let tabBarHeight = isIPhone5OrSmaller ? CGFloat(32) : CGFloat(48) @objc public static let settingButtonHeight = isIPhone5OrSmaller ? CGFloat(52) : CGFloat(75) + @objc public static let defaultSettingRowHeight = CGFloat(60) @objc public static let modalCornerRadius = CGFloat(10) @objc public static let modalButtonCornerRadius = CGFloat(5) @objc public static let fakeChatBubbleWidth = CGFloat(224) diff --git a/SessionUtilitiesKit/General/UIEdgeInsets.swift b/SessionUtilitiesKit/General/UIEdgeInsets.swift new file mode 100644 index 000000000..86f2029c0 --- /dev/null +++ b/SessionUtilitiesKit/General/UIEdgeInsets.swift @@ -0,0 +1,8 @@ +import UIKit + +extension UIEdgeInsets { + + public init(uniform value: CGFloat) { + self.init(top: value, left: value, bottom: value, right: value) + } +}