diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 34f85867973774239257e839c970c995dc013de4..8516acc715a65ba6d62d6b8c03f95cefcf645dfe 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -16,20 +16,12 @@ 02674C851E0C757B0065EDF9 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02674C811E0C757B0065EDF9 /* RxCocoa.framework */; }; 02674C861E0C757B0065EDF9 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02674C821E0C757B0065EDF9 /* RxSwift.framework */; }; 0273C2FF1E0C438F00CF00BA /* AccountAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0273C2FE1E0C438F00CF00BA /* AccountAdapterDelegate.swift */; }; - 0273C3051E0C68B100CF00BA /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0273C3031E0C68B100CF00BA /* CreateProfileViewController.swift */; }; - 0273C3061E0C68B100CF00BA /* CreateRingAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0273C3041E0C68B100CF00BA /* CreateRingAccountViewController.swift */; }; 0273C3081E0C68BF00CF00BA /* DesignableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0273C3071E0C68BF00CF00BA /* DesignableButton.swift */; }; 029CE9D71E1D8C860000C8E1 /* ServiceEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029CE9D61E1D8C860000C8E1 /* ServiceEventTests.swift */; }; 02AED8191DD4C4B100F740BA /* librestbed.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 02AED8181DD4C4B100F740BA /* librestbed.a */; }; - 02B22DFC1DF755BB000358C9 /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22DFA1DF755BB000358C9 /* AccountModel.swift */; }; - 02B22DFD1DF755BB000358C9 /* CreateRingAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22DFB1DF755BB000358C9 /* CreateRingAccountViewModel.swift */; }; 02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22DFE1DF755DB000358C9 /* AccountsService.swift */; }; - 02B22E011DF755E5000358C9 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22E001DF755E5000358C9 /* MainTabBarViewController.swift */; }; 02B22E091DF7585F000358C9 /* DaemonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22E081DF7585F000358C9 /* DaemonService.swift */; }; 02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C9B63E1E1D4E8C00F82F0C /* ServiceEvent.swift */; }; - 02DD80C81E1EAD70009A3510 /* AccountConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD80C71E1EAD70009A3510 /* AccountConfigModel.swift */; }; - 02DD80CA1E1EAF1A009A3510 /* AccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD80C91E1EAF1A009A3510 /* AccountCredentialsModel.swift */; }; - 02DD80CD1E1EB2E4009A3510 /* ConfigKeyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD80CC1E1EB2E4009A3510 /* ConfigKeyModel.swift */; }; 02E1A0251DDE4ABA00D75B59 /* String+Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043866371D2304A700E06CE2 /* String+Bool.swift */; }; 043866211D218B1100E06CE2 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 043866201D218B1100E06CE2 /* AudioToolbox.framework */; }; 043999F71D1C2D9D00E99CD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043999F61D1C2D9D00E99CD9 /* AppDelegate.swift */; }; @@ -91,35 +83,88 @@ 04399B131D1C341A00E99CD9 /* libvpx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE11D1C341A00E99CD9 /* libvpx.a */; }; 04399B141D1C341A00E99CD9 /* libx264.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE21D1C341A00E99CD9 /* libx264.a */; }; 04399B151D1C341A00E99CD9 /* libyaml-cpp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */; }; + 1A0C4EDA1F1D4B1B00550433 /* WelcomeViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A0C4ED91F1D4B1B00550433 /* WelcomeViewController.storyboard */; }; + 1A0C4EDC1F1D4B7E00550433 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4EDB1F1D4B7E00550433 /* WelcomeViewController.swift */; }; + 1A0C4EE31F1D673600550433 /* InjectionBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4EE21F1D673600550433 /* InjectionBag.swift */; }; + 1A0C4EE51F1D67DF00550433 /* WalkthroughCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4EE41F1D67DF00550433 /* WalkthroughCoordinator.swift */; }; 1A1E476D1F0E808500EA9A36 /* Reusable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A1E476C1F0E808500EA9A36 /* Reusable.framework */; }; 1A1E476F1F0E894600EA9A36 /* SwiftyBeaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A1E476E1F0E894600EA9A36 /* SwiftyBeaver.framework */; }; + 1A20417A1F1E547F00C08435 /* Stateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2041791F1E547F00C08435 /* Stateable.swift */; }; + 1A20417C1F1E56FF00C08435 /* WelcomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A20417B1F1E56FF00C08435 /* WelcomeViewModel.swift */; }; + 1A20417E1F1E8DDA00C08435 /* CreateProfileViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A20417D1F1E8DDA00C08435 /* CreateProfileViewController.storyboard */; }; + 1A2041801F1E903B00C08435 /* CreateProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A20417F1F1E903B00C08435 /* CreateProfileViewController.swift */; }; + 1A2041821F1E906B00C08435 /* CreateProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2041811F1E906B00C08435 /* CreateProfileViewModel.swift */; }; + 1A2041841F1EA0FC00C08435 /* CreateAccountViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2041831F1EA0FC00C08435 /* CreateAccountViewController.storyboard */; }; + 1A2041861F1EA19600C08435 /* CreateAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2041851F1EA19600C08435 /* CreateAccountViewController.swift */; }; + 1A2041881F1EA1EA00C08435 /* CreateAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2041871F1EA1EA00C08435 /* CreateAccountViewModel.swift */; }; + 1A20418B1F1EA58A00C08435 /* ViewModelBased.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A20418A1F1EA58A00C08435 /* ViewModelBased.swift */; }; + 1A20418D1F1EABCC00C08435 /* StateableResponsive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A20418C1F1EABCC00C08435 /* StateableResponsive.swift */; }; + 1A20418F1F1EAC0E00C08435 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A20418E1F1EAC0E00C08435 /* Coordinator.swift */; }; + 1A2041911F1FD46300C08435 /* DesignableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2041901F1FD46200C08435 /* DesignableView.swift */; }; + 1A2D189A1F2642C000B2C785 /* NotificationCenter+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18991F2642C000B2C785 /* NotificationCenter+Ring.swift */; }; + 1A2D189C1F264AD900B2C785 /* UIViewController+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D189B1F264AD900B2C785 /* UIViewController+Ring.swift */; }; + 1A2D18A01F27A6D600B2C785 /* LinkDeviceViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D189D1F27A6D600B2C785 /* LinkDeviceViewController.storyboard */; }; + 1A2D18A11F27A6D600B2C785 /* LinkDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D189E1F27A6D600B2C785 /* LinkDeviceViewController.swift */; }; + 1A2D18A21F27A6D600B2C785 /* LinkDeviceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D189F1F27A6D600B2C785 /* LinkDeviceViewModel.swift */; }; + 1A2D18A41F27EF5200B2C785 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18A31F27EF5200B2C785 /* AppCoordinator.swift */; }; + 1A2D18A61F27F7A400B2C785 /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18A51F27F7A400B2C785 /* UIViewController+Rx.swift */; }; + 1A2D18AA1F29131900B2C785 /* ConversationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18A91F29131900B2C785 /* ConversationsCoordinator.swift */; }; + 1A2D18AC1F29149D00B2C785 /* MeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18AB1F29149D00B2C785 /* MeCoordinator.swift */; }; + 1A2D18B11F2915B600B2C785 /* SmartlistViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18B01F2915B600B2C785 /* SmartlistViewController.storyboard */; }; + 1A2D18B31F2915C500B2C785 /* ConversationViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18B21F2915C500B2C785 /* ConversationViewController.storyboard */; }; + 1A2D18B71F29164700B2C785 /* SmartlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18B51F29164700B2C785 /* SmartlistViewModel.swift */; }; + 1A2D18C11F29180700B2C785 /* AccountConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18B91F29180700B2C785 /* AccountConfigModel.swift */; }; + 1A2D18C21F29180700B2C785 /* AccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BA1F29180700B2C785 /* AccountCredentialsModel.swift */; }; + 1A2D18C31F29180700B2C785 /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BB1F29180700B2C785 /* AccountModel.swift */; }; + 1A2D18C41F29180700B2C785 /* ConfigKeyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BC1F29180700B2C785 /* ConfigKeyModel.swift */; }; + 1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BD1F29180700B2C785 /* ContactModel.swift */; }; + 1A2D18C61F29180700B2C785 /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BE1F29180700B2C785 /* ConversationModel.swift */; }; + 1A2D18C71F29180700B2C785 /* DeviceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18BF1F29180700B2C785 /* DeviceModel.swift */; }; + 1A2D18D11F29182500B2C785 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */; }; + 1A2D18D81F2918EE00B2C785 /* MeDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18D71F2918EE00B2C785 /* MeDetailViewController.swift */; }; + 1A2D18DD1F29192D00B2C785 /* MessableBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18DC1F29192D00B2C785 /* MessableBubble.swift */; }; + 1A2D18E51F29197100B2C785 /* MessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18DE1F29197100B2C785 /* MessageAccessoryView.swift */; }; + 1A2D18E61F29197100B2C785 /* MessageAccessoryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18DF1F29197100B2C785 /* MessageAccessoryView.xib */; }; + 1A2D18EB1F29197100B2C785 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18E41F29197100B2C785 /* MessageViewModel.swift */; }; + 1A2D18ED1F2919D800B2C785 /* MeViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18EC1F2919D800B2C785 /* MeViewController.storyboard */; }; + 1A2D18EF1F291A0100B2C785 /* MeDetailViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18EE1F291A0100B2C785 /* MeDetailViewController.storyboard */; }; + 1A2D18F51F292D7200B2C785 /* MessageCellReceived.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18F11F292D7200B2C785 /* MessageCellReceived.swift */; }; + 1A2D18F61F292D7200B2C785 /* MessageCellReceived.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18F21F292D7200B2C785 /* MessageCellReceived.xib */; }; + 1A2D18F71F292D7200B2C785 /* MessageCellSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18F31F292D7200B2C785 /* MessageCellSent.swift */; }; + 1A2D18F81F292D7200B2C785 /* MessageCellSent.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18F41F292D7200B2C785 /* MessageCellSent.xib */; }; + 1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */; }; + 1A2D18FD1F292DAD00B2C785 /* ConversationCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18FB1F292DAD00B2C785 /* ConversationCell.xib */; }; + 1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18FE1F29352D00B2C785 /* MeViewModel.swift */; }; + 1A2D19011F29353A00B2C785 /* MeDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D19001F29353A00B2C785 /* MeDetailViewModel.swift */; }; + 1A2D19041F2937DF00B2C785 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D19031F2937DF00B2C785 /* AccountTableViewCell.swift */; }; 1A3CA32B1F102BB700283748 /* Chameleon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3CA32A1F102BB700283748 /* Chameleon.framework */; }; 1A3CA32D1F13DA7200283748 /* Chameleon+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3CA32C1F13DA7200283748 /* Chameleon+Ring.swift */; }; - 1A3CA32F1F1400AF00283748 /* MessageCellReceived.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A3CA32E1F1400AF00283748 /* MessageCellReceived.xib */; }; - 1A3CA3351F140EB300283748 /* MessableBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3CA3341F140EB300283748 /* MessableBubble.swift */; }; - 1A3CA3381F14133300283748 /* MessageCellSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3CA3361F14133300283748 /* MessageCellSent.swift */; }; - 1A3CA3391F14133300283748 /* MessageCellSent.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A3CA3371F14133300283748 /* MessageCellSent.xib */; }; 1A3D28A71F0EB9DB00B524EE /* Bool+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3D28A61F0EB9DB00B524EE /* Bool+String.swift */; }; 1A3D28A91F0EBF0200B524EE /* UIView+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3D28A81F0EBF0200B524EE /* UIView+Ring.swift */; }; - 1A8306331F0EDAA50099D98C /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8306321F0EDAA50099D98C /* AccountTableViewCell.swift */; }; + 1A5DC01E1F355DA70075E8EF /* ContactsAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */; }; + 1A5DC0201F355DCF0075E8EF /* ContactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */; }; + 1A5DC0241F3564360075E8EF /* ContactRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0231F3564360075E8EF /* ContactRequestModel.swift */; }; + 1A5DC0281F3564AA0075E8EF /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0271F3564AA0075E8EF /* MessageModel.swift */; }; + 1A5DC02C1F3565250075E8EF /* MeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC02B1F3565250075E8EF /* MeViewController.swift */; }; + 1A5DC02E1F3565640075E8EF /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC02D1F3565640075E8EF /* ConversationViewModel.swift */; }; + 1A5DC0301F3565AE0075E8EF /* SmartlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */; }; + 1A5DC0321F3566140075E8EF /* ConversationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0311F3566140075E8EF /* ConversationSection.swift */; }; + 1A5DC0371F35675E0075E8EF /* ContactRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0351F35675E0075E8EF /* ContactRequestCell.swift */; }; + 1A5DC0381F35675E0075E8EF /* ContactRequestCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A5DC0361F35675E0075E8EF /* ContactRequestCell.xib */; }; + 1A5DC03D1F35678D0075E8EF /* ContactRequestItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0391F35678D0075E8EF /* ContactRequestItem.swift */; }; + 1A5DC03E1F35678D0075E8EF /* ContactRequestsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A5DC03A1F35678D0075E8EF /* ContactRequestsViewController.storyboard */; }; + 1A5DC03F1F35678D0075E8EF /* ContactRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC03B1F35678D0075E8EF /* ContactRequestsViewController.swift */; }; + 1A5DC0401F35678D0075E8EF /* ContactRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC03C1F35678D0075E8EF /* ContactRequestsViewModel.swift */; }; + 1A5DC0421F3567DF0075E8EF /* ContactRequestsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DC0411F3567DF0075E8EF /* ContactRequestsCoordinator.swift */; }; 1AABA7461F0FE9C000739605 /* UIColor+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AABA7451F0FE9C000739605 /* UIColor+Ring.swift */; }; - 1ABE07BC1F0C22CC00D36361 /* WalkthroughStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07BB1F0C22CC00D36361 /* WalkthroughStoryboard.storyboard */; }; 1ABE07D21F0D8FE800D36361 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABE07D01F0D8FE800D36361 /* Images.swift */; }; 1ABE07D31F0D8FE800D36361 /* Storyboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABE07D11F0D8FE800D36361 /* Storyboards.swift */; }; 1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DA1F0D915100D36361 /* Localizable.strings */; }; 1ABE07DF1F0D91A800D36361 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */; }; - 1ABE07E01F0D91A800D36361 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DE1F0D91A800D36361 /* Main.storyboard */; }; 1ABE07E21F0D924700D36361 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABE07E11F0D924700D36361 /* Strings.swift */; }; 5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */; }; 5557FD4A1E81AE850043E394 /* AccountModelHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */; }; 557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */; }; - 5604AE6B1F1963D900B15965 /* ContactRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5604AE6A1F1963D900B15965 /* ContactRequestsViewModel.swift */; }; - 5604AE721F1D057F00B15965 /* ContactRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5604AE701F1D057F00B15965 /* ContactRequestCell.swift */; }; - 5604AE731F1D057F00B15965 /* ContactRequestCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5604AE711F1D057F00B15965 /* ContactRequestCell.xib */; }; - 5628B41C1F0C358D008B1E11 /* AccountDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5628B41A1F0C358D008B1E11 /* AccountDetailsViewController.swift */; }; - 5628B41D1F0C358D008B1E11 /* MeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5628B41B1F0C358D008B1E11 /* MeViewController.swift */; }; - 5628B4211F0C35C8008B1E11 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5628B4201F0C35C8008B1E11 /* WelcomeViewController.swift */; }; - 562FB6CD1EFAD18A00C61A78 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562FB6CC1EFAD18A00C61A78 /* ConversationViewController.swift */; }; 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56308BA61EA00E5700660275 /* NameRegistrationResponse.m */; }; 563AEC771EA664C0003A5641 /* RegistrationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 563AEC761EA664C0003A5641 /* RegistrationResponse.m */; }; 564775831EE5CFC500A0C855 /* Realm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 564775811EE5CFC500A0C855 /* Realm.framework */; }; @@ -129,41 +174,15 @@ 564C44621E943DE6000F92B1 /* NameService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44611E943DE6000F92B1 /* NameService.swift */; }; 564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */; }; 56559B0E1EE8777600BF20E1 /* RxRealm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56559B0D1EE8777600BF20E1 /* RxRealm.framework */; }; - 56559B141EE89E7900BF20E1 /* DeviceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56559B131EE89E7900BF20E1 /* DeviceModel.swift */; }; - 5669A7FA1EA904AF003C7B93 /* SwitchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7F91EA904AF003C7B93 /* SwitchCell.xib */; }; - 5669A7FC1EA904D2003C7B93 /* TextFieldCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7FB1EA904D2003C7B93 /* TextFieldCell.xib */; }; - 5669A7FE1EA904E4003C7B93 /* TextCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7FD1EA904E4003C7B93 /* TextCell.xib */; }; - 5669A8031EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5669A8021EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift */; }; - 566FE4291F0E97640091B4D1 /* ContactRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566FE4281F0E97640091B4D1 /* ContactRequestModel.swift */; }; 568F56751EA7E5DE00132D7D /* PKHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 568F56721EA7E38F00132D7D /* PKHUD.framework */; }; - 569806001F213101001870AC /* ContactRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5604AE6E1F1D01D100B15965 /* ContactRequestsViewController.swift */; }; - 569806021F223D9E001870AC /* ContactRequestItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569806011F223D9E001870AC /* ContactRequestItem.swift */; }; - 56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */; }; - 56AC64E11E80542300EA1AA9 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */; }; - 56AC64E31E805F0200EA1AA9 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E21E805F0200EA1AA9 /* TextCell.swift */; }; 56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC650D1E85694D00EA1AA9 /* DesignableTextField.swift */; }; 56BBC99F1ED714CB00CDAF8B /* MessagesAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC99E1ED714CB00CDAF8B /* MessagesAdapter.mm */; }; 56BBC9A21ED714DF00CDAF8B /* MessagesAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A01ED714DF00CDAF8B /* MessagesAdapterDelegate.swift */; }; 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A11ED714DF00CDAF8B /* ConversationsService.swift */; }; - 56BBC9A61ED7151500CDAF8B /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */; }; - 56BBC9A81ED7152300CDAF8B /* SmartlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */; }; - 56BBC9B01ED7155700CDAF8B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */; }; - 56BBC9B41ED7156500CDAF8B /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */; }; - 56BBC9B51ED7156500CDAF8B /* ConversationCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */; }; - 56BBC9B91ED715FE00CDAF8B /* ContactModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */; }; 56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9BB1ED7161200CDAF8B /* Date+Helpers.swift */; }; - 56BBC9BF1ED7168400CDAF8B /* SmartlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */; }; - 56BBC9CD1EDC5E7000CDAF8B /* MessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9C71EDC5E7000CDAF8B /* MessageAccessoryView.swift */; }; - 56BBC9CE1EDC5E7000CDAF8B /* MessageAccessoryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 56BBC9C81EDC5E7000CDAF8B /* MessageAccessoryView.xib */; }; - 56BBC9CF1EDC5E7000CDAF8B /* MessageCellReceived.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9C91EDC5E7000CDAF8B /* MessageCellReceived.swift */; }; - 56BBC9D21EDC5E7000CDAF8B /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9CC1EDC5E7000CDAF8B /* MessageViewModel.swift */; }; 56BBC9D41EDC7A6D00CDAF8B /* libargon2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56BBC9D31EDC7A6D00CDAF8B /* libargon2.a */; }; 56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */; }; - 56BBC9E01EDDC9E600CDAF8B /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */; }; - 56BBC9E31EDDCC8100CDAF8B /* ConversationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */; }; 56C715FF1F0D36C600770048 /* ContactsAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */; }; - 56C716011F0D36D900770048 /* ContactsAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */; }; - 56C716031F0D466100770048 /* ContactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C716021F0D466100770048 /* ContactsService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -198,21 +217,13 @@ 02674C831E0C757B0065EDF9 /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxTest.framework; path = Carthage/Build/iOS/RxTest.framework; sourceTree = "<group>"; }; 0273C2FE1E0C438F00CF00BA /* AccountAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountAdapterDelegate.swift; sourceTree = "<group>"; }; 0273C3001E0C445200CF00BA /* RingPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RingPrefixHeader.pch; path = Ring/RingPrefixHeader.pch; sourceTree = "<group>"; }; - 0273C3031E0C68B100CF00BA /* CreateProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = "<group>"; }; - 0273C3041E0C68B100CF00BA /* CreateRingAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRingAccountViewController.swift; sourceTree = "<group>"; }; 0273C3071E0C68BF00CF00BA /* DesignableButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DesignableButton.swift; sourceTree = "<group>"; }; 028568301DF610A9003A8D8D /* RingTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RingTests-Bridging-Header.h"; sourceTree = "<group>"; }; 029CE9D61E1D8C860000C8E1 /* ServiceEventTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceEventTests.swift; sourceTree = "<group>"; }; 02AED8181DD4C4B100F740BA /* librestbed.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = librestbed.a; path = ../DEPS/x86_64/lib/librestbed.a; sourceTree = "<group>"; }; - 02B22DFA1DF755BB000358C9 /* AccountModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModel.swift; sourceTree = "<group>"; }; - 02B22DFB1DF755BB000358C9 /* CreateRingAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRingAccountViewModel.swift; sourceTree = "<group>"; }; 02B22DFE1DF755DB000358C9 /* AccountsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsService.swift; sourceTree = "<group>"; }; - 02B22E001DF755E5000358C9 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; }; 02B22E081DF7585F000358C9 /* DaemonService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DaemonService.swift; sourceTree = "<group>"; }; 02C9B63E1E1D4E8C00F82F0C /* ServiceEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceEvent.swift; sourceTree = "<group>"; }; - 02DD80C71E1EAD70009A3510 /* AccountConfigModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountConfigModel.swift; sourceTree = "<group>"; }; - 02DD80C91E1EAF1A009A3510 /* AccountCredentialsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountCredentialsModel.swift; sourceTree = "<group>"; }; - 02DD80CC1E1EB2E4009A3510 /* ConfigKeyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigKeyModel.swift; sourceTree = "<group>"; }; 043866201D218B1100E06CE2 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 043866371D2304A700E06CE2 /* String+Bool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Bool.swift"; sourceTree = "<group>"; }; 043999F31D1C2D9D00E99CD9 /* Ring.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ring.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -284,37 +295,91 @@ 04399AE11D1C341A00E99CD9 /* libvpx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libvpx.a; path = ../fat/lib/libvpx.a; sourceTree = "<group>"; }; 04399AE21D1C341A00E99CD9 /* libx264.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libx264.a; path = ../fat/lib/libx264.a; sourceTree = "<group>"; }; 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libyaml-cpp.a"; path = "../fat/lib/libyaml-cpp.a"; sourceTree = "<group>"; }; + 1A0C4ED91F1D4B1B00550433 /* WelcomeViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = WelcomeViewController.storyboard; path = Welcome/WelcomeViewController.storyboard; sourceTree = "<group>"; }; + 1A0C4EDB1F1D4B7E00550433 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WelcomeViewController.swift; path = Welcome/WelcomeViewController.swift; sourceTree = "<group>"; }; + 1A0C4EE21F1D673600550433 /* InjectionBag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InjectionBag.swift; sourceTree = "<group>"; }; + 1A0C4EE41F1D67DF00550433 /* WalkthroughCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalkthroughCoordinator.swift; sourceTree = "<group>"; }; 1A1E476C1F0E808500EA9A36 /* Reusable.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reusable.framework; path = Carthage/Build/iOS/Reusable.framework; sourceTree = "<group>"; }; 1A1E476E1F0E894600EA9A36 /* SwiftyBeaver.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyBeaver.framework; path = Carthage/Build/iOS/SwiftyBeaver.framework; sourceTree = "<group>"; }; + 1A2041791F1E547F00C08435 /* Stateable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stateable.swift; sourceTree = "<group>"; }; + 1A20417B1F1E56FF00C08435 /* WelcomeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WelcomeViewModel.swift; path = Welcome/WelcomeViewModel.swift; sourceTree = "<group>"; }; + 1A20417D1F1E8DDA00C08435 /* CreateProfileViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = CreateProfileViewController.storyboard; sourceTree = "<group>"; }; + 1A20417F1F1E903B00C08435 /* CreateProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateProfileViewController.swift; sourceTree = "<group>"; }; + 1A2041811F1E906B00C08435 /* CreateProfileViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateProfileViewModel.swift; sourceTree = "<group>"; }; + 1A2041831F1EA0FC00C08435 /* CreateAccountViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = CreateAccountViewController.storyboard; sourceTree = "<group>"; }; + 1A2041851F1EA19600C08435 /* CreateAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountViewController.swift; sourceTree = "<group>"; }; + 1A2041871F1EA1EA00C08435 /* CreateAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountViewModel.swift; sourceTree = "<group>"; }; + 1A20418A1F1EA58A00C08435 /* ViewModelBased.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelBased.swift; sourceTree = "<group>"; }; + 1A20418C1F1EABCC00C08435 /* StateableResponsive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateableResponsive.swift; sourceTree = "<group>"; }; + 1A20418E1F1EAC0E00C08435 /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; }; + 1A2041901F1FD46200C08435 /* DesignableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DesignableView.swift; sourceTree = "<group>"; }; + 1A2D18991F2642C000B2C785 /* NotificationCenter+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Ring.swift"; sourceTree = "<group>"; }; + 1A2D189B1F264AD900B2C785 /* UIViewController+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ring.swift"; sourceTree = "<group>"; }; + 1A2D189D1F27A6D600B2C785 /* LinkDeviceViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LinkDeviceViewController.storyboard; sourceTree = "<group>"; }; + 1A2D189E1F27A6D600B2C785 /* LinkDeviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceViewController.swift; sourceTree = "<group>"; }; + 1A2D189F1F27A6D600B2C785 /* LinkDeviceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceViewModel.swift; sourceTree = "<group>"; }; + 1A2D18A31F27EF5200B2C785 /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; }; + 1A2D18A51F27F7A400B2C785 /* UIViewController+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = "<group>"; }; + 1A2D18A91F29131900B2C785 /* ConversationsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationsCoordinator.swift; sourceTree = "<group>"; }; + 1A2D18AB1F29149D00B2C785 /* MeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeCoordinator.swift; sourceTree = "<group>"; }; + 1A2D18B01F2915B600B2C785 /* SmartlistViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SmartlistViewController.storyboard; sourceTree = "<group>"; }; + 1A2D18B21F2915C500B2C785 /* ConversationViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ConversationViewController.storyboard; sourceTree = "<group>"; }; + 1A2D18B51F29164700B2C785 /* SmartlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartlistViewModel.swift; sourceTree = "<group>"; }; + 1A2D18B91F29180700B2C785 /* AccountConfigModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountConfigModel.swift; sourceTree = "<group>"; }; + 1A2D18BA1F29180700B2C785 /* AccountCredentialsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountCredentialsModel.swift; sourceTree = "<group>"; }; + 1A2D18BB1F29180700B2C785 /* AccountModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModel.swift; sourceTree = "<group>"; }; + 1A2D18BC1F29180700B2C785 /* ConfigKeyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigKeyModel.swift; sourceTree = "<group>"; }; + 1A2D18BD1F29180700B2C785 /* ContactModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactModel.swift; sourceTree = "<group>"; }; + 1A2D18BE1F29180700B2C785 /* ConversationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; }; + 1A2D18BF1F29180700B2C785 /* DeviceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceModel.swift; sourceTree = "<group>"; }; + 1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; }; + 1A2D18D71F2918EE00B2C785 /* MeDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeDetailViewController.swift; sourceTree = "<group>"; }; + 1A2D18DC1F29192D00B2C785 /* MessableBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessableBubble.swift; sourceTree = "<group>"; }; + 1A2D18DE1F29197100B2C785 /* MessageAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageAccessoryView.swift; sourceTree = "<group>"; }; + 1A2D18DF1F29197100B2C785 /* MessageAccessoryView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageAccessoryView.xib; sourceTree = "<group>"; }; + 1A2D18E41F29197100B2C785 /* MessageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; }; + 1A2D18EC1F2919D800B2C785 /* MeViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MeViewController.storyboard; sourceTree = "<group>"; }; + 1A2D18EE1F291A0100B2C785 /* MeDetailViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MeDetailViewController.storyboard; sourceTree = "<group>"; }; + 1A2D18F11F292D7200B2C785 /* MessageCellReceived.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellReceived.swift; sourceTree = "<group>"; }; + 1A2D18F21F292D7200B2C785 /* MessageCellReceived.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellReceived.xib; sourceTree = "<group>"; }; + 1A2D18F31F292D7200B2C785 /* MessageCellSent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellSent.swift; sourceTree = "<group>"; }; + 1A2D18F41F292D7200B2C785 /* MessageCellSent.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellSent.xib; sourceTree = "<group>"; }; + 1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; }; + 1A2D18FB1F292DAD00B2C785 /* ConversationCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ConversationCell.xib; sourceTree = "<group>"; }; + 1A2D18FE1F29352D00B2C785 /* MeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeViewModel.swift; sourceTree = "<group>"; }; + 1A2D19001F29353A00B2C785 /* MeDetailViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeDetailViewModel.swift; sourceTree = "<group>"; }; + 1A2D19031F2937DF00B2C785 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; }; 1A3CA32A1F102BB700283748 /* Chameleon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Chameleon.framework; path = Carthage/Build/iOS/Chameleon.framework; sourceTree = "<group>"; }; 1A3CA32C1F13DA7200283748 /* Chameleon+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Chameleon+Ring.swift"; sourceTree = "<group>"; }; - 1A3CA32E1F1400AF00283748 /* MessageCellReceived.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellReceived.xib; sourceTree = "<group>"; }; - 1A3CA3341F140EB300283748 /* MessableBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessableBubble.swift; sourceTree = "<group>"; }; - 1A3CA3361F14133300283748 /* MessageCellSent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellSent.swift; sourceTree = "<group>"; }; - 1A3CA3371F14133300283748 /* MessageCellSent.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellSent.xib; sourceTree = "<group>"; }; 1A3D28A61F0EB9DB00B524EE /* Bool+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bool+String.swift"; sourceTree = "<group>"; }; 1A3D28A81F0EBF0200B524EE /* UIView+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Ring.swift"; sourceTree = "<group>"; }; - 1A8306321F0EDAA50099D98C /* AccountTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; }; + 1A5DC00A1F3558980075E8EF /* ContactsAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContactsAdapter.h; sourceTree = "<group>"; }; + 1A5DC00D1F3559070075E8EF /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; }; + 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; }; + 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsService.swift; sourceTree = "<group>"; }; + 1A5DC0231F3564360075E8EF /* ContactRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestModel.swift; sourceTree = "<group>"; }; + 1A5DC0271F3564AA0075E8EF /* MessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = "<group>"; }; + 1A5DC02B1F3565250075E8EF /* MeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeViewController.swift; sourceTree = "<group>"; }; + 1A5DC02D1F3565640075E8EF /* ConversationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; }; + 1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SmartlistViewController.swift; path = Ring/Features/Conversations/SmartList/SmartlistViewController.swift; sourceTree = SOURCE_ROOT; }; + 1A5DC0311F3566140075E8EF /* ConversationSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConversationSection.swift; path = Ring/Features/Conversations/SmartList/ConversationSection.swift; sourceTree = SOURCE_ROOT; }; + 1A5DC0351F35675E0075E8EF /* ContactRequestCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestCell.swift; sourceTree = "<group>"; }; + 1A5DC0361F35675E0075E8EF /* ContactRequestCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactRequestCell.xib; sourceTree = "<group>"; }; + 1A5DC0391F35678D0075E8EF /* ContactRequestItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestItem.swift; sourceTree = "<group>"; }; + 1A5DC03A1F35678D0075E8EF /* ContactRequestsViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContactRequestsViewController.storyboard; sourceTree = "<group>"; }; + 1A5DC03B1F35678D0075E8EF /* ContactRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestsViewController.swift; sourceTree = "<group>"; }; + 1A5DC03C1F35678D0075E8EF /* ContactRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestsViewModel.swift; sourceTree = "<group>"; }; + 1A5DC0411F3567DF0075E8EF /* ContactRequestsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestsCoordinator.swift; sourceTree = "<group>"; }; 1AABA7451F0FE9C000739605 /* UIColor+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Ring.swift"; sourceTree = "<group>"; }; - 1ABE07BB1F0C22CC00D36361 /* WalkthroughStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = WalkthroughStoryboard.storyboard; sourceTree = "<group>"; }; 1ABE07D01F0D8FE800D36361 /* Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = "<group>"; }; 1ABE07D11F0D8FE800D36361 /* Storyboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storyboards.swift; sourceTree = "<group>"; }; 1ABE07DB1F0D915100D36361 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Resources/LaunchScreen.storyboard; sourceTree = "<group>"; }; - 1ABE07DE1F0D91A800D36361 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Resources/Main.storyboard; sourceTree = "<group>"; }; 1ABE07E11F0D924700D36361 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; }; 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModelHelper.swift; sourceTree = "<group>"; }; 5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModelHelperTests.swift; sourceTree = "<group>"; }; 557086501E8ADB9D001A7CE4 /* SystemAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemAdapter.h; sourceTree = "<group>"; }; 557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemAdapter.mm; sourceTree = "<group>"; }; - 5604AE6A1F1963D900B15965 /* ContactRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestsViewModel.swift; sourceTree = "<group>"; }; - 5604AE6E1F1D01D100B15965 /* ContactRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestsViewController.swift; sourceTree = "<group>"; }; - 5604AE701F1D057F00B15965 /* ContactRequestCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestCell.swift; sourceTree = "<group>"; }; - 5604AE711F1D057F00B15965 /* ContactRequestCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactRequestCell.xib; sourceTree = "<group>"; }; - 5628B41A1F0C358D008B1E11 /* AccountDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDetailsViewController.swift; sourceTree = "<group>"; }; - 5628B41B1F0C358D008B1E11 /* MeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeViewController.swift; sourceTree = "<group>"; }; - 5628B4201F0C35C8008B1E11 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; }; - 562FB6CC1EFAD18A00C61A78 /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; }; 56308BA51EA00E5700660275 /* NameRegistrationResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NameRegistrationResponse.h; sourceTree = "<group>"; }; 56308BA61EA00E5700660275 /* NameRegistrationResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NameRegistrationResponse.m; sourceTree = "<group>"; }; 563AEC751EA664C0003A5641 /* RegistrationResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegistrationResponse.h; sourceTree = "<group>"; }; @@ -327,39 +392,16 @@ 564C44611E943DE6000F92B1 /* NameService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameService.swift; sourceTree = "<group>"; }; 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameRegistrationAdapterDelegate.swift; sourceTree = "<group>"; }; 56559B0D1EE8777600BF20E1 /* RxRealm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxRealm.framework; path = Carthage/Build/iOS/RxRealm.framework; sourceTree = "<group>"; }; - 56559B131EE89E7900BF20E1 /* DeviceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceModel.swift; sourceTree = "<group>"; }; - 5669A7F91EA904AF003C7B93 /* SwitchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchCell.xib; sourceTree = "<group>"; }; - 5669A7FB1EA904D2003C7B93 /* TextFieldCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextFieldCell.xib; sourceTree = "<group>"; }; - 5669A7FD1EA904E4003C7B93 /* TextCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextCell.xib; sourceTree = "<group>"; }; - 5669A8021EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceToAccountViewController.swift; sourceTree = "<group>"; }; - 566FE4281F0E97640091B4D1 /* ContactRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestModel.swift; sourceTree = "<group>"; }; 568F56721EA7E38F00132D7D /* PKHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PKHUD.framework; path = Carthage/Build/iOS/PKHUD.framework; sourceTree = "<group>"; }; - 569806011F223D9E001870AC /* ContactRequestItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestItem.swift; sourceTree = "<group>"; }; - 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; }; - 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldCell.swift; sourceTree = "<group>"; }; - 56AC64E21E805F0200EA1AA9 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = "<group>"; }; 56AC650D1E85694D00EA1AA9 /* DesignableTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DesignableTextField.swift; sourceTree = "<group>"; }; 56BBC99D1ED714CB00CDAF8B /* MessagesAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessagesAdapter.h; sourceTree = "<group>"; }; 56BBC99E1ED714CB00CDAF8B /* MessagesAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MessagesAdapter.mm; sourceTree = "<group>"; }; 56BBC9A01ED714DF00CDAF8B /* MessagesAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesAdapterDelegate.swift; sourceTree = "<group>"; }; 56BBC9A11ED714DF00CDAF8B /* ConversationsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationsService.swift; sourceTree = "<group>"; }; - 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = "<group>"; }; - 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartlistViewController.swift; sourceTree = "<group>"; }; - 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; }; - 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; }; - 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; }; - 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ConversationCell.xib; sourceTree = "<group>"; }; - 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactModel.swift; sourceTree = "<group>"; }; 56BBC9BB1ED7161200CDAF8B /* Date+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Helpers.swift"; sourceTree = "<group>"; }; - 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartlistViewModel.swift; sourceTree = "<group>"; }; - 56BBC9C71EDC5E7000CDAF8B /* MessageAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageAccessoryView.swift; sourceTree = "<group>"; }; - 56BBC9C81EDC5E7000CDAF8B /* MessageAccessoryView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageAccessoryView.xib; sourceTree = "<group>"; }; - 56BBC9C91EDC5E7000CDAF8B /* MessageCellReceived.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellReceived.swift; sourceTree = "<group>"; }; - 56BBC9CC1EDC5E7000CDAF8B /* MessageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; }; 56BBC9D31EDC7A6D00CDAF8B /* libargon2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libargon2.a; path = ../fat/lib/libargon2.a; sourceTree = "<group>"; }; 56BBC9DD1EDDC9D300CDAF8B /* LookupNameResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LookupNameResponse.h; sourceTree = "<group>"; }; 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LookupNameResponse.m; sourceTree = "<group>"; }; - 56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSection.swift; sourceTree = "<group>"; }; 56C715FD1F0D36C600770048 /* ContactsAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsAdapter.h; sourceTree = "<group>"; }; 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; }; 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; }; @@ -458,19 +500,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0273C3021E0C689600CF00BA /* Walkthrough */ = { - isa = PBXGroup; - children = ( - 56AC64DD1E804EB500EA1AA9 /* Cells */, - 5628B4201F0C35C8008B1E11 /* WelcomeViewController.swift */, - 0273C3031E0C68B100CF00BA /* CreateProfileViewController.swift */, - 0273C3041E0C68B100CF00BA /* CreateRingAccountViewController.swift */, - 5669A8021EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift */, - 1ABE07BB1F0C22CC00D36361 /* WalkthroughStoryboard.storyboard */, - ); - path = Walkthrough; - sourceTree = "<group>"; - }; 028568231DF60E5C003A8D8D /* Fixtures */ = { isa = PBXGroup; children = ( @@ -522,6 +551,8 @@ 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */, 56C716021F0D466100770048 /* ContactsService.swift */, 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */, + 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */, + 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */, ); path = Services; sourceTree = "<group>"; @@ -529,14 +560,7 @@ 02E1A0271DDE4C3900D75B59 /* Account */ = { isa = PBXGroup; children = ( - 1A8306321F0EDAA50099D98C /* AccountTableViewCell.swift */, - 02B22DFA1DF755BB000358C9 /* AccountModel.swift */, 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */, - 02B22DFB1DF755BB000358C9 /* CreateRingAccountViewModel.swift */, - 02DD80C71E1EAD70009A3510 /* AccountConfigModel.swift */, - 02DD80C91E1EAF1A009A3510 /* AccountCredentialsModel.swift */, - 02DD80CC1E1EB2E4009A3510 /* ConfigKeyModel.swift */, - 56559B131EE89E7900BF20E1 /* DeviceModel.swift */, ); path = Account; sourceTree = "<group>"; @@ -555,25 +579,21 @@ 04399AA81D1C304300E99CD9 /* DRingAdapter.mm */, 04399AAA1D1C304300E99CD9 /* Utils.h */, 04399AAB1D1C304300E99CD9 /* Utils.mm */, + 1A5DC00A1F3558980075E8EF /* ContactsAdapter.h */, + 1A5DC00D1F3559070075E8EF /* ContactsAdapter.mm */, 563AEC741EA66487003A5641 /* AccountCreation */, 563AEC731EA6627F003A5641 /* NameRegistration */, ); path = Bridging; sourceTree = "<group>"; }; - 02F9B1C21DDDFF0E00FE123D /* MainTabBar */ = { - isa = PBXGroup; - children = ( - 02B22E001DF755E5000358C9 /* MainTabBarViewController.swift */, - ); - path = MainTabBar; - sourceTree = "<group>"; - }; 043866341D22D04E00E06CE2 /* UI */ = { isa = PBXGroup; children = ( + 1A2D18DC1F29192D00B2C785 /* MessableBubble.swift */, 0273C3071E0C68BF00CF00BA /* DesignableButton.swift */, 56AC650D1E85694D00EA1AA9 /* DesignableTextField.swift */, + 1A2041901F1FD46200C08435 /* DesignableView.swift */, ); path = UI; sourceTree = "<group>"; @@ -587,6 +607,9 @@ 1A3D28A81F0EBF0200B524EE /* UIView+Ring.swift */, 1AABA7451F0FE9C000739605 /* UIColor+Ring.swift */, 1A3CA32C1F13DA7200283748 /* Chameleon+Ring.swift */, + 1A2D18991F2642C000B2C785 /* NotificationCenter+Ring.swift */, + 1A2D189B1F264AD900B2C785 /* UIViewController+Ring.swift */, + 1A2D18A51F27F7A400B2C785 /* UIViewController+Rx.swift */, ); path = Extensions; sourceTree = "<group>"; @@ -618,23 +641,19 @@ 043999F51D1C2D9D00E99CD9 /* Ring */ = { isa = PBXGroup; children = ( - 1ABE07C61F0D86B300D36361 /* Resources */, - 56BBC9BD1ED7165800CDAF8B /* Smartlist */, - 56BBC9B61ED7158600CDAF8B /* Contacts */, - 56BBC9AD1ED7154800CDAF8B /* Conversations */, - 56BBC9A41ED7150200CDAF8B /* Messages */, + 02E1A0261DDE4C2E00D75B59 /* Services */, + 1A2D18B81F2916BA00B2C785 /* Models */, 564C44571E8D7F68000F92B1 /* Constants */, - 0273C3021E0C689600CF00BA /* Walkthrough */, + 043866391D2307C000E06CE2 /* Extensions */, + 1A2041891F1EA51E00C08435 /* Protocols */, + 1A0C4EDF1F1D624100550433 /* Coordinators */, + 1A0C4EBC1F1D48AA00550433 /* Features */, + 1ABE07C61F0D86B300D36361 /* Resources */, 02EFCACF1E0C3DD600FD8ED1 /* Bridging */, 02E1A0271DDE4C3900D75B59 /* Account */, - 02E1A0261DDE4C2E00D75B59 /* Services */, - 02F9B1C21DDDFF0E00FE123D /* MainTabBar */, 043866341D22D04E00E06CE2 /* UI */, - 043866391D2307C000E06CE2 /* Extensions */, - 5628B4191F0C358D008B1E11 /* Settings */, 043999F61D1C2D9D00E99CD9 /* AppDelegate.swift */, 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */, - 1ABE07DE1F0D91A800D36361 /* Main.storyboard */, 04399A071D1C2D9D00E99CD9 /* Info.plist */, ); path = Ring; @@ -734,133 +753,279 @@ name = SYS_DEPS; sourceTree = "<group>"; }; - 1ABE07C51F0D862D00D36361 /* Generated */ = { + 1A0C4EBC1F1D48AA00550433 /* Features */ = { isa = PBXGroup; children = ( - 1ABE07E11F0D924700D36361 /* Strings.swift */, - 1ABE07D01F0D8FE800D36361 /* Images.swift */, - 1ABE07D11F0D8FE800D36361 /* Storyboards.swift */, + 1A0C4EBD1F1D48DD00550433 /* Walkthrough */, + 1A2D18A71F290FAA00B2C785 /* Conversations */, + 1A5DC0331F3567080075E8EF /* ContactRequests */, + 1A2D18A81F290FBF00B2C785 /* Me */, ); - path = Generated; + name = Features; sourceTree = "<group>"; }; - 1ABE07C61F0D86B300D36361 /* Resources */ = { + 1A0C4EBD1F1D48DD00550433 /* Walkthrough */ = { isa = PBXGroup; children = ( - 1ABE07DA1F0D915100D36361 /* Localizable.strings */, - 04399A021D1C2D9D00E99CD9 /* Images.xcassets */, + 1A0C4ED51F1D49D800550433 /* Welcome */, + 1A0C4ED71F1D4AC300550433 /* CreateProfile */, + 1A0C4ED61F1D4AB300550433 /* CreateAccount */, + 1A0C4ED81F1D4AD600550433 /* LinkDevice */, + 1A0C4EE41F1D67DF00550433 /* WalkthroughCoordinator.swift */, ); - path = Resources; + name = Walkthrough; + path = Features/Walkthrough; sourceTree = "<group>"; }; - 5628B4191F0C358D008B1E11 /* Settings */ = { + 1A0C4ED51F1D49D800550433 /* Welcome */ = { isa = PBXGroup; children = ( - 5628B41A1F0C358D008B1E11 /* AccountDetailsViewController.swift */, - 5628B41B1F0C358D008B1E11 /* MeViewController.swift */, + 1A0C4ED91F1D4B1B00550433 /* WelcomeViewController.storyboard */, + 1A0C4EDB1F1D4B7E00550433 /* WelcomeViewController.swift */, + 1A20417B1F1E56FF00C08435 /* WelcomeViewModel.swift */, ); - path = Settings; + name = Welcome; sourceTree = "<group>"; }; - 563AEC731EA6627F003A5641 /* NameRegistration */ = { + 1A0C4ED61F1D4AB300550433 /* CreateAccount */ = { isa = PBXGroup; children = ( - 56BBC9DD1EDDC9D300CDAF8B /* LookupNameResponse.h */, - 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */, - 564C445E1E943C37000F92B1 /* NameRegistrationAdapter.h */, - 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */, - 56308BA51EA00E5700660275 /* NameRegistrationResponse.h */, - 56308BA61EA00E5700660275 /* NameRegistrationResponse.m */, + 1A2041831F1EA0FC00C08435 /* CreateAccountViewController.storyboard */, + 1A2041851F1EA19600C08435 /* CreateAccountViewController.swift */, + 1A2041871F1EA1EA00C08435 /* CreateAccountViewModel.swift */, ); - path = NameRegistration; + path = CreateAccount; sourceTree = "<group>"; }; - 563AEC741EA66487003A5641 /* AccountCreation */ = { + 1A0C4ED71F1D4AC300550433 /* CreateProfile */ = { isa = PBXGroup; children = ( - 04399AA51D1C304300E99CD9 /* AccountAdapter.h */, - 04399AA61D1C304300E99CD9 /* AccountAdapter.mm */, - 563AEC751EA664C0003A5641 /* RegistrationResponse.h */, - 563AEC761EA664C0003A5641 /* RegistrationResponse.m */, + 1A20417D1F1E8DDA00C08435 /* CreateProfileViewController.storyboard */, + 1A20417F1F1E903B00C08435 /* CreateProfileViewController.swift */, + 1A2041811F1E906B00C08435 /* CreateProfileViewModel.swift */, ); - path = AccountCreation; + path = CreateProfile; sourceTree = "<group>"; }; - 564C44571E8D7F68000F92B1 /* Constants */ = { + 1A0C4ED81F1D4AD600550433 /* LinkDevice */ = { isa = PBXGroup; children = ( - 1ABE07C51F0D862D00D36361 /* Generated */, - 564C445A1E8EA44E000F92B1 /* Durations.swift */, + 1A2D189D1F27A6D600B2C785 /* LinkDeviceViewController.storyboard */, + 1A2D189E1F27A6D600B2C785 /* LinkDeviceViewController.swift */, + 1A2D189F1F27A6D600B2C785 /* LinkDeviceViewModel.swift */, ); - path = Constants; + path = LinkDevice; sourceTree = "<group>"; }; - 56AC64DD1E804EB500EA1AA9 /* Cells */ = { + 1A0C4EDF1F1D624100550433 /* Coordinators */ = { isa = PBXGroup; children = ( - 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */, - 5669A7F91EA904AF003C7B93 /* SwitchCell.xib */, - 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */, - 5669A7FB1EA904D2003C7B93 /* TextFieldCell.xib */, - 56AC64E21E805F0200EA1AA9 /* TextCell.swift */, - 5669A7FD1EA904E4003C7B93 /* TextCell.xib */, + 1A0C4EE21F1D673600550433 /* InjectionBag.swift */, + 1A2D18A31F27EF5200B2C785 /* AppCoordinator.swift */, ); - name = Cells; - path = Cell; + path = Coordinators; sourceTree = "<group>"; }; - 56BBC9A41ED7150200CDAF8B /* Messages */ = { + 1A2041891F1EA51E00C08435 /* Protocols */ = { isa = PBXGroup; children = ( - 56BBC9C71EDC5E7000CDAF8B /* MessageAccessoryView.swift */, - 56BBC9C81EDC5E7000CDAF8B /* MessageAccessoryView.xib */, - 1A3CA32E1F1400AF00283748 /* MessageCellReceived.xib */, - 56BBC9C91EDC5E7000CDAF8B /* MessageCellReceived.swift */, - 1A3CA3361F14133300283748 /* MessageCellSent.swift */, - 1A3CA3371F14133300283748 /* MessageCellSent.xib */, - 56BBC9CC1EDC5E7000CDAF8B /* MessageViewModel.swift */, - 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */, - 1A3CA3341F140EB300283748 /* MessableBubble.swift */, - ); - path = Messages; + 1A20418A1F1EA58A00C08435 /* ViewModelBased.swift */, + 1A2041791F1E547F00C08435 /* Stateable.swift */, + 1A20418C1F1EABCC00C08435 /* StateableResponsive.swift */, + 1A20418E1F1EAC0E00C08435 /* Coordinator.swift */, + ); + path = Protocols; sourceTree = "<group>"; }; - 56BBC9AD1ED7154800CDAF8B /* Conversations */ = { + 1A2D18A71F290FAA00B2C785 /* Conversations */ = { isa = PBXGroup; children = ( - 562FB6CC1EFAD18A00C61A78 /* ConversationViewController.swift */, - 56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */, - 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */, - 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */, - 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */, - 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */, - ); - path = Conversations; + 1A2D18AE1F29153F00B2C785 /* Conversation */, + 1A2D18AD1F29151E00B2C785 /* Smartlist */, + 1A2D18A91F29131900B2C785 /* ConversationsCoordinator.swift */, + ); + name = Conversations; + path = Features/Conversations; sourceTree = "<group>"; }; - 56BBC9B61ED7158600CDAF8B /* Contacts */ = { + 1A2D18A81F290FBF00B2C785 /* Me */ = { isa = PBXGroup; children = ( - 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */, - 566FE4281F0E97640091B4D1 /* ContactRequestModel.swift */, - 569806011F223D9E001870AC /* ContactRequestItem.swift */, - 5604AE6A1F1963D900B15965 /* ContactRequestsViewModel.swift */, - 5604AE6E1F1D01D100B15965 /* ContactRequestsViewController.swift */, - 5604AE701F1D057F00B15965 /* ContactRequestCell.swift */, - 5604AE711F1D057F00B15965 /* ContactRequestCell.xib */, - ); - path = Contacts; + 1A2D18D91F2918F300B2C785 /* Me */, + 1A2D18AF1F29158700B2C785 /* Detail */, + 1A2D18AB1F29149D00B2C785 /* MeCoordinator.swift */, + ); + name = Me; + path = Features/Me; sourceTree = "<group>"; }; - 56BBC9BD1ED7165800CDAF8B /* Smartlist */ = { + 1A2D18AD1F29151E00B2C785 /* Smartlist */ = { isa = PBXGroup; children = ( - 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */, - 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */, + 1A2D18F91F292DA000B2C785 /* Cells */, + 1A2D18B01F2915B600B2C785 /* SmartlistViewController.storyboard */, + 1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */, + 1A2D18B51F29164700B2C785 /* SmartlistViewModel.swift */, + 1A5DC0311F3566140075E8EF /* ConversationSection.swift */, ); path = Smartlist; sourceTree = "<group>"; }; + 1A2D18AE1F29153F00B2C785 /* Conversation */ = { + isa = PBXGroup; + children = ( + 1A2D18F01F292D6100B2C785 /* Cells */, + 1A2D18B21F2915C500B2C785 /* ConversationViewController.storyboard */, + 1A2D18CC1F29182500B2C785 /* ConversationViewController.swift */, + 1A5DC02D1F3565640075E8EF /* ConversationViewModel.swift */, + 1A2D18DE1F29197100B2C785 /* MessageAccessoryView.swift */, + 1A2D18DF1F29197100B2C785 /* MessageAccessoryView.xib */, + 1A2D18E41F29197100B2C785 /* MessageViewModel.swift */, + ); + path = Conversation; + sourceTree = "<group>"; + }; + 1A2D18AF1F29158700B2C785 /* Detail */ = { + isa = PBXGroup; + children = ( + 1A2D18EE1F291A0100B2C785 /* MeDetailViewController.storyboard */, + 1A2D18D71F2918EE00B2C785 /* MeDetailViewController.swift */, + 1A2D19001F29353A00B2C785 /* MeDetailViewModel.swift */, + ); + path = Detail; + sourceTree = "<group>"; + }; + 1A2D18B81F2916BA00B2C785 /* Models */ = { + isa = PBXGroup; + children = ( + 1A5DC0271F3564AA0075E8EF /* MessageModel.swift */, + 1A5DC0231F3564360075E8EF /* ContactRequestModel.swift */, + 1A2D18B91F29180700B2C785 /* AccountConfigModel.swift */, + 1A2D18BA1F29180700B2C785 /* AccountCredentialsModel.swift */, + 1A2D18BB1F29180700B2C785 /* AccountModel.swift */, + 1A2D18BC1F29180700B2C785 /* ConfigKeyModel.swift */, + 1A2D18BD1F29180700B2C785 /* ContactModel.swift */, + 1A2D18BE1F29180700B2C785 /* ConversationModel.swift */, + 1A2D18BF1F29180700B2C785 /* DeviceModel.swift */, + ); + path = Models; + sourceTree = "<group>"; + }; + 1A2D18D91F2918F300B2C785 /* Me */ = { + isa = PBXGroup; + children = ( + 1A2D19021F2937BB00B2C785 /* Cells */, + 1A2D18EC1F2919D800B2C785 /* MeViewController.storyboard */, + 1A5DC02B1F3565250075E8EF /* MeViewController.swift */, + 1A2D18FE1F29352D00B2C785 /* MeViewModel.swift */, + ); + path = Me; + sourceTree = "<group>"; + }; + 1A2D18F01F292D6100B2C785 /* Cells */ = { + isa = PBXGroup; + children = ( + 1A2D18F11F292D7200B2C785 /* MessageCellReceived.swift */, + 1A2D18F21F292D7200B2C785 /* MessageCellReceived.xib */, + 1A2D18F31F292D7200B2C785 /* MessageCellSent.swift */, + 1A2D18F41F292D7200B2C785 /* MessageCellSent.xib */, + ); + path = Cells; + sourceTree = "<group>"; + }; + 1A2D18F91F292DA000B2C785 /* Cells */ = { + isa = PBXGroup; + children = ( + 1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */, + 1A2D18FB1F292DAD00B2C785 /* ConversationCell.xib */, + ); + name = Cells; + path = ../SmartList/Cells; + sourceTree = "<group>"; + }; + 1A2D19021F2937BB00B2C785 /* Cells */ = { + isa = PBXGroup; + children = ( + 1A2D19031F2937DF00B2C785 /* AccountTableViewCell.swift */, + ); + path = Cells; + sourceTree = "<group>"; + }; + 1A5DC0331F3567080075E8EF /* ContactRequests */ = { + isa = PBXGroup; + children = ( + 1A5DC0341F3567360075E8EF /* Cells */, + 1A5DC0411F3567DF0075E8EF /* ContactRequestsCoordinator.swift */, + 1A5DC03A1F35678D0075E8EF /* ContactRequestsViewController.storyboard */, + 1A5DC03B1F35678D0075E8EF /* ContactRequestsViewController.swift */, + 1A5DC03C1F35678D0075E8EF /* ContactRequestsViewModel.swift */, + 1A5DC0391F35678D0075E8EF /* ContactRequestItem.swift */, + ); + name = ContactRequests; + path = Features/ContactRequests; + sourceTree = "<group>"; + }; + 1A5DC0341F3567360075E8EF /* Cells */ = { + isa = PBXGroup; + children = ( + 1A5DC0351F35675E0075E8EF /* ContactRequestCell.swift */, + 1A5DC0361F35675E0075E8EF /* ContactRequestCell.xib */, + ); + path = Cells; + sourceTree = "<group>"; + }; + 1ABE07C51F0D862D00D36361 /* Generated */ = { + isa = PBXGroup; + children = ( + 1ABE07E11F0D924700D36361 /* Strings.swift */, + 1ABE07D01F0D8FE800D36361 /* Images.swift */, + 1ABE07D11F0D8FE800D36361 /* Storyboards.swift */, + ); + path = Generated; + sourceTree = "<group>"; + }; + 1ABE07C61F0D86B300D36361 /* Resources */ = { + isa = PBXGroup; + children = ( + 1ABE07DA1F0D915100D36361 /* Localizable.strings */, + 04399A021D1C2D9D00E99CD9 /* Images.xcassets */, + ); + path = Resources; + sourceTree = "<group>"; + }; + 563AEC731EA6627F003A5641 /* NameRegistration */ = { + isa = PBXGroup; + children = ( + 56BBC9DD1EDDC9D300CDAF8B /* LookupNameResponse.h */, + 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */, + 564C445E1E943C37000F92B1 /* NameRegistrationAdapter.h */, + 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */, + 56308BA51EA00E5700660275 /* NameRegistrationResponse.h */, + 56308BA61EA00E5700660275 /* NameRegistrationResponse.m */, + ); + path = NameRegistration; + sourceTree = "<group>"; + }; + 563AEC741EA66487003A5641 /* AccountCreation */ = { + isa = PBXGroup; + children = ( + 04399AA51D1C304300E99CD9 /* AccountAdapter.h */, + 04399AA61D1C304300E99CD9 /* AccountAdapter.mm */, + 563AEC751EA664C0003A5641 /* RegistrationResponse.h */, + 563AEC761EA664C0003A5641 /* RegistrationResponse.m */, + ); + path = AccountCreation; + sourceTree = "<group>"; + }; + 564C44571E8D7F68000F92B1 /* Constants */ = { + isa = PBXGroup; + children = ( + 1ABE07C51F0D862D00D36361 /* Generated */, + 564C445A1E8EA44E000F92B1 /* Durations.swift */, + ); + path = Constants; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -976,19 +1141,23 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5604AE731F1D057F00B15965 /* ContactRequestCell.xib in Resources */, - 5669A7FE1EA904E4003C7B93 /* TextCell.xib in Resources */, + 1A2D18FD1F292DAD00B2C785 /* ConversationCell.xib in Resources */, 1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */, - 56BBC9CE1EDC5E7000CDAF8B /* MessageAccessoryView.xib in Resources */, - 1A3CA32F1F1400AF00283748 /* MessageCellReceived.xib in Resources */, - 1A3CA3391F14133300283748 /* MessageCellSent.xib in Resources */, + 1A2D18E61F29197100B2C785 /* MessageAccessoryView.xib in Resources */, + 1A2D18F81F292D7200B2C785 /* MessageCellSent.xib in Resources */, + 1A2D18F61F292D7200B2C785 /* MessageCellReceived.xib in Resources */, + 1A2D18EF1F291A0100B2C785 /* MeDetailViewController.storyboard in Resources */, + 1A2D18B11F2915B600B2C785 /* SmartlistViewController.storyboard in Resources */, 04399A031D1C2D9D00E99CD9 /* Images.xcassets in Resources */, - 1ABE07E01F0D91A800D36361 /* Main.storyboard in Resources */, - 5669A7FA1EA904AF003C7B93 /* SwitchCell.xib in Resources */, - 56BBC9B51ED7156500CDAF8B /* ConversationCell.xib in Resources */, + 1A2041841F1EA0FC00C08435 /* CreateAccountViewController.storyboard in Resources */, + 1A2D18ED1F2919D800B2C785 /* MeViewController.storyboard in Resources */, + 1A20417E1F1E8DDA00C08435 /* CreateProfileViewController.storyboard in Resources */, 1ABE07DF1F0D91A800D36361 /* LaunchScreen.storyboard in Resources */, - 5669A7FC1EA904D2003C7B93 /* TextFieldCell.xib in Resources */, - 1ABE07BC1F0C22CC00D36361 /* WalkthroughStoryboard.storyboard in Resources */, + 1A5DC0381F35675E0075E8EF /* ContactRequestCell.xib in Resources */, + 1A0C4EDA1F1D4B1B00550433 /* WelcomeViewController.storyboard in Resources */, + 1A5DC03E1F35678D0075E8EF /* ContactRequestsViewController.storyboard in Resources */, + 1A2D18B31F2915C500B2C785 /* ConversationViewController.storyboard in Resources */, + 1A2D18A01F27A6D600B2C785 /* LinkDeviceViewController.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1079,74 +1248,89 @@ buildActionMask = 2147483647; files = ( 557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */, - 5604AE6B1F1963D900B15965 /* ContactRequestsViewModel.swift in Sources */, - 5669A8031EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift in Sources */, - 0273C3051E0C68B100CF00BA /* CreateProfileViewController.swift in Sources */, + 1A2D18AC1F29149D00B2C785 /* MeCoordinator.swift in Sources */, + 1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */, + 1A2D18F71F292D7200B2C785 /* MessageCellSent.swift in Sources */, 02E1A0251DDE4ABA00D75B59 /* String+Bool.swift in Sources */, 04399AAC1D1C304300E99CD9 /* AccountAdapter.mm in Sources */, 1AABA7461F0FE9C000739605 /* UIColor+Ring.swift in Sources */, - 02DD80C81E1EAD70009A3510 /* AccountConfigModel.swift in Sources */, + 1A5DC0201F355DCF0075E8EF /* ContactsService.swift in Sources */, + 1A2D18C71F29180700B2C785 /* DeviceModel.swift in Sources */, + 1A20418F1F1EAC0E00C08435 /* Coordinator.swift in Sources */, + 1A2D18A11F27A6D600B2C785 /* LinkDeviceViewController.swift in Sources */, + 1A0C4EDC1F1D4B7E00550433 /* WelcomeViewController.swift in Sources */, + 1A2D18D81F2918EE00B2C785 /* MeDetailViewController.swift in Sources */, 02B22E091DF7585F000358C9 /* DaemonService.swift in Sources */, - 0273C3061E0C68B100CF00BA /* CreateRingAccountViewController.swift in Sources */, 56BBC99F1ED714CB00CDAF8B /* MessagesAdapter.mm in Sources */, - 1A3CA3351F140EB300283748 /* MessableBubble.swift in Sources */, - 56BBC9CF1EDC5E7000CDAF8B /* MessageCellReceived.swift in Sources */, + 1A2D18A61F27F7A400B2C785 /* UIViewController+Rx.swift in Sources */, + 1A5DC0241F3564360075E8EF /* ContactRequestModel.swift in Sources */, + 1A5DC03F1F35678D0075E8EF /* ContactRequestsViewController.swift in Sources */, + 1A20418B1F1EA58A00C08435 /* ViewModelBased.swift in Sources */, + 1A5DC01E1F355DA70075E8EF /* ContactsAdapterDelegate.swift in Sources */, + 1A20418D1F1EABCC00C08435 /* StateableResponsive.swift in Sources */, + 1A0C4EE51F1D67DF00550433 /* WalkthroughCoordinator.swift in Sources */, + 1A2D18DD1F29192D00B2C785 /* MessableBubble.swift in Sources */, + 1A5DC02E1F3565640075E8EF /* ConversationViewModel.swift in Sources */, + 1A2D189C1F264AD900B2C785 /* UIViewController+Ring.swift in Sources */, 02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */, + 1A2D18C11F29180700B2C785 /* AccountConfigModel.swift in Sources */, 1A3D28A71F0EB9DB00B524EE /* Bool+String.swift in Sources */, - 566FE4291F0E97640091B4D1 /* ContactRequestModel.swift in Sources */, - 56BBC9D21EDC5E7000CDAF8B /* MessageViewModel.swift in Sources */, - 569806021F223D9E001870AC /* ContactRequestItem.swift in Sources */, - 02DD80CD1E1EB2E4009A3510 /* ConfigKeyModel.swift in Sources */, - 56BBC9E31EDDCC8100CDAF8B /* ConversationSection.swift in Sources */, - 56BBC9A81ED7152300CDAF8B /* SmartlistViewController.swift in Sources */, 5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */, 1ABE07D31F0D8FE800D36361 /* Storyboards.swift in Sources */, - 56559B141EE89E7900BF20E1 /* DeviceModel.swift in Sources */, + 1A2D18E51F29197100B2C785 /* MessageAccessoryView.swift in Sources */, + 1A2D18C61F29180700B2C785 /* ConversationModel.swift in Sources */, 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */, - 5628B4211F0C35C8008B1E11 /* WelcomeViewController.swift in Sources */, 1A3CA32D1F13DA7200283748 /* Chameleon+Ring.swift in Sources */, - 5628B41C1F0C358D008B1E11 /* AccountDetailsViewController.swift in Sources */, - 56C716031F0D466100770048 /* ContactsService.swift in Sources */, - 56AC64E11E80542300EA1AA9 /* TextFieldCell.swift in Sources */, 1ABE07E21F0D924700D36361 /* Strings.swift in Sources */, - 56AC64E31E805F0200EA1AA9 /* TextCell.swift in Sources */, 56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */, - 56BBC9BF1ED7168400CDAF8B /* SmartlistViewModel.swift in Sources */, - 02B22E011DF755E5000358C9 /* MainTabBarViewController.swift in Sources */, - 56C716011F0D36D900770048 /* ContactsAdapterDelegate.swift in Sources */, + 1A2D189A1F2642C000B2C785 /* NotificationCenter+Ring.swift in Sources */, + 1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */, + 1A5DC0371F35675E0075E8EF /* ContactRequestCell.swift in Sources */, + 1A20417C1F1E56FF00C08435 /* WelcomeViewModel.swift in Sources */, + 1A5DC03D1F35678D0075E8EF /* ContactRequestItem.swift in Sources */, + 1A2041821F1E906B00C08435 /* CreateProfileViewModel.swift in Sources */, + 1A0C4EE31F1D673600550433 /* InjectionBag.swift in Sources */, 564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */, - 1A3CA3381F14133300283748 /* MessageCellSent.swift in Sources */, + 1A2D19041F2937DF00B2C785 /* AccountTableViewCell.swift in Sources */, + 1A2D18AA1F29131900B2C785 /* ConversationsCoordinator.swift in Sources */, 043999F71D1C2D9D00E99CD9 /* AppDelegate.swift in Sources */, - 02B22DFC1DF755BB000358C9 /* AccountModel.swift in Sources */, - 56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */, - 56BBC9B91ED715FE00CDAF8B /* ContactModel.swift in Sources */, + 1A2041861F1EA19600C08435 /* CreateAccountViewController.swift in Sources */, + 1A2D18C21F29180700B2C785 /* AccountCredentialsModel.swift in Sources */, + 1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */, + 1A2D18B71F29164700B2C785 /* SmartlistViewModel.swift in Sources */, 04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */, 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */, - 56BBC9B01ED7155700CDAF8B /* ConversationModel.swift in Sources */, 563AEC771EA664C0003A5641 /* RegistrationResponse.m in Sources */, - 02B22DFD1DF755BB000358C9 /* CreateRingAccountViewModel.swift in Sources */, 564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */, - 1A8306331F0EDAA50099D98C /* AccountTableViewCell.swift in Sources */, 56C715FF1F0D36C600770048 /* ContactsAdapter.mm in Sources */, + 1A5DC0281F3564AA0075E8EF /* MessageModel.swift in Sources */, 56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */, + 1A2041911F1FD46300C08435 /* DesignableView.swift in Sources */, 1A3D28A91F0EBF0200B524EE /* UIView+Ring.swift in Sources */, + 1A2041881F1EA1EA00C08435 /* CreateAccountViewModel.swift in Sources */, 1ABE07D21F0D8FE800D36361 /* Images.swift in Sources */, - 56BBC9CD1EDC5E7000CDAF8B /* MessageAccessoryView.swift in Sources */, - 02DD80CA1E1EAF1A009A3510 /* AccountCredentialsModel.swift in Sources */, 0273C3081E0C68BF00CF00BA /* DesignableButton.swift in Sources */, + 1A5DC0321F3566140075E8EF /* ConversationSection.swift in Sources */, + 1A2D18C41F29180700B2C785 /* ConfigKeyModel.swift in Sources */, + 1A20417A1F1E547F00C08435 /* Stateable.swift in Sources */, + 1A2D18F51F292D7200B2C785 /* MessageCellReceived.swift in Sources */, 56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */, - 562FB6CD1EFAD18A00C61A78 /* ConversationViewController.swift in Sources */, 564C44621E943DE6000F92B1 /* NameService.swift in Sources */, - 56BBC9E01EDDC9E600CDAF8B /* ConversationViewModel.swift in Sources */, + 1A5DC02C1F3565250075E8EF /* MeViewController.swift in Sources */, + 1A2041801F1E903B00C08435 /* CreateProfileViewController.swift in Sources */, 04399AAD1D1C304300E99CD9 /* DRingAdapter.mm in Sources */, + 1A2D18D11F29182500B2C785 /* ConversationViewController.swift in Sources */, 0273C2FF1E0C438F00CF00BA /* AccountAdapterDelegate.swift in Sources */, + 1A2D19011F29353A00B2C785 /* MeDetailViewModel.swift in Sources */, + 1A2D18A41F27EF5200B2C785 /* AppCoordinator.swift in Sources */, + 1A2D18C31F29180700B2C785 /* AccountModel.swift in Sources */, + 1A2D18EB1F29197100B2C785 /* MessageViewModel.swift in Sources */, 02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */, - 5628B41D1F0C358D008B1E11 /* MeViewController.swift in Sources */, - 569806001F213101001870AC /* ContactRequestsViewController.swift in Sources */, - 56BBC9A61ED7151500CDAF8B /* MessageModel.swift in Sources */, - 5604AE721F1D057F00B15965 /* ContactRequestCell.swift in Sources */, + 1A5DC0421F3567DF0075E8EF /* ContactRequestsCoordinator.swift in Sources */, + 1A5DC0401F35678D0075E8EF /* ContactRequestsViewModel.swift in Sources */, 56BBC9A21ED714DF00CDAF8B /* MessagesAdapterDelegate.swift in Sources */, - 56BBC9B41ED7156500CDAF8B /* ConversationCell.swift in Sources */, + 1A5DC0301F3565AE0075E8EF /* SmartlistViewController.swift in Sources */, + 1A2D18A21F27A6D600B2C785 /* LinkDeviceViewModel.swift in Sources */, 564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Ring/Ring/Account/CreateRingAccountViewModel.swift b/Ring/Ring/Account/CreateRingAccountViewModel.swift deleted file mode 100644 index 8667557525e9e3f8e99ac23893f9637155f35dca..0000000000000000000000000000000000000000 --- a/Ring/Ring/Account/CreateRingAccountViewModel.swift +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (C) 2017 Savoir-faire Linux Inc. - * - * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -import RxSwift - -/** - A class representing the ViewModel (MVVM) of the accounts managed by Ring. - Its responsabilities: - - expose to the Views a public API for its interactions concerning the Accounts, - - react to the Views user events concerning the Accounts (add an account...) - */ -class CreateRingAccountViewModel { - /** - Dispose bag that contains the Disposable objects of the ViewModel, and managing their disposes. - */ - fileprivate let disposeBag = DisposeBag() - - /** - Retains the currently active stream adding an account. - Useful to dispose it before starting a new one. - */ - fileprivate var addAccountDisposable: Disposable? - - /** - The account under this ViewModel. - */ - fileprivate var account: AccountModel? - - /** - The accountService instance injected in initializer. - */ - fileprivate let accountService: AccountsService - - /** - The nameService instance injected in initializer. - */ - fileprivate var nameService: NameService - - // MARK: - Rx Variables and Observers - - var username = Variable<String>("") - var password = Variable<String>("") - var repeatPassword = Variable<String>("") - - var passwordValid: Observable<Bool>! - var passwordsEqual: Observable<Bool>! - var canCreateAccount: Observable<Bool>! - var registerUsername = Variable<Bool>(true) - - var hasNewPassword: Observable<Bool>! - var hidePasswordError: Observable<Bool>! - var hideRepeatPasswordError: Observable<Bool>! - - var accountCreationState = PublishSubject<AccountCreationState>() - - /** - Message presented to the user in function of the status of the current username lookup request - */ - var usernameValidationMessage: Observable<String>! - - // MARK: - - - /** - Default constructor - */ - init(withAccountService accountService: AccountsService, nameService: NameService) { - self.account = nil - self.accountService = accountService - self.nameService = nameService - self.initObservables() - self.initObservers() - } - - /** - Constructor with AccountModel. - */ - init(withAccountService accountService: AccountsService, - accountModel account: AccountModel?, nameService: NameService) { - self.account = account - self.accountService = accountService - self.nameService = nameService - self.initObservables() - self.initObservers() - } - - /** - Start the process of account creation - */ - func createAccount() { - - //Add account - accountCreationState.onNext(.started) - self.accountService.addRingAccount(withUsername: self.username.value, - password: self.password.value) - } - - /** - Init obsevables needed to validate the user inputs for account creation - */ - fileprivate func initObservables() { - - self.passwordValid = password.asObservable().map { password in - return password.characters.count >= 6 - }.shareReplay(1).observeOn(MainScheduler.instance) - - self.passwordsEqual = Observable<Bool>.combineLatest(self.password.asObservable(), - self.repeatPassword.asObservable()) { password, repeatPassword in - return password == repeatPassword - }.shareReplay(1).observeOn(MainScheduler.instance) - - self.canCreateAccount = Observable<Bool>.combineLatest(self.registerUsername.asObservable(), - self.nameService.usernameValidationStatus, - self.passwordValid, - self.passwordsEqual) { registerUsername, usernameValidationStatus, passwordValid, passwordsEquals in - if registerUsername { - return usernameValidationStatus == .valid && passwordValid && passwordsEquals - } else { - return passwordValid && passwordsEquals - } - }.shareReplay(1).observeOn(MainScheduler.instance) - - self.usernameValidationMessage = self.nameService.usernameValidationStatus - .asObservable().map ({ status in - switch status { - case .lookingUp: - return L10n.Createaccount.lookingForUsernameAvailability - case .invalid: - return L10n.Createaccount.invalidUsername - case .alreadyTaken: - return L10n.Createaccount.usernameAlreadyTaken - default: - return "" - } - }).shareReplay(1).observeOn(MainScheduler.instance) - - hasNewPassword = self.password.asObservable().map({ password in - return !password.characters.isEmpty - }) - - hidePasswordError = Observable<Bool>.combineLatest(self.passwordValid, hasNewPassword) { isPasswordValid, hasNewPassword in - return isPasswordValid || !hasNewPassword - } - - let hasRepeatPassword = self.repeatPassword.asObservable().map({ repeatPassword in - return !repeatPassword.characters.isEmpty - }) - - hideRepeatPasswordError = Observable<Bool>.combineLatest(self.passwordValid, self.passwordsEqual, hasRepeatPassword) { isPasswordValid, isPasswordsEquals, hasRepeatPassword in - return !isPasswordValid || isPasswordsEquals || !hasRepeatPassword - } - } - - /** - Init observers for account creation - */ - fileprivate func initObservers() { - - //Loookup name request observer - self.username.asObservable().subscribe(onNext: { [unowned self] username in - self.nameService.lookupName(withAccount: "", nameserver: "", name: username) - }).disposed(by: disposeBag) - - //Name registration observer - self.accountService - .sharedResponseStream - .filter({ event in - return event.eventType == ServiceEventType.registrationStateChanged && - event.getEventInput(ServiceEventInput.registrationState) == Unregistered && - self.registerUsername.value - }) - .subscribe(onNext: { [unowned self] _ in - - //Launch the process of name registration - if let currentAccountId = self.accountService.currentAccount?.id { - self.nameService.registerName(withAccount: currentAccountId, - password: self.password.value, - name: self.username.value) - } - }) - .disposed(by: disposeBag) - - //Account creation state observer - self.accountService - .sharedResponseStream - .subscribe(onNext: { [unowned self] event in - if event.getEventInput(ServiceEventInput.registrationState) == Unregistered { - self.accountCreationState.onNext(.success) - } else if event.getEventInput(ServiceEventInput.registrationState) == ErrorGeneric { - self.accountCreationState.onError(AccountCreationError.generic) - } else if event.getEventInput(ServiceEventInput.registrationState) == ErrorNetwork { - self.accountCreationState.onError(AccountCreationError.network) - } - }, onError: { _ in - self.accountCreationState.onError(AccountCreationError.unknown) - }).disposed(by: disposeBag) - } -} - -// MARK: Account Creation state - -enum AccountCreationState { - case started - case success - case error(error: AccountCreationError) -} - -enum AccountCreationError: Error { - case generic - case network - case unknown -} - -extension AccountCreationError: LocalizedError { - - var title: String { - switch self { - case .generic: - return L10n.Alerts.accountCannotBeFoundTitle - case .network: - return L10n.Alerts.accountNoNetworkTitle - default: - return L10n.Alerts.accountDefaultErrorTitle - } - } - - var message: String { - switch self { - case .generic: - return L10n.Alerts.accountDefaultErrorMessage - case .network: - return L10n.Alerts.accountNoNetworkMessage - default: - return L10n.Alerts.accountDefaultErrorMessage - } - } -} diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift index 679e6dacc124614028907b51e3bcfccd8423a0f9..2c4c76c9b126acbe68eedbd3a682978f2c08fd65 100644 --- a/Ring/Ring/AppDelegate.swift +++ b/Ring/Ring/AppDelegate.swift @@ -1,8 +1,9 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com> * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +25,7 @@ import RealmSwift import SwiftyBeaver import RxSwift import Chameleon +import Contacts import Contacts @@ -31,11 +33,22 @@ import Contacts class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - static let daemonService = DaemonService(dRingAdaptor: DRingAdapter()) - static let accountService = AccountsService(withAccountAdapter: AccountAdapter()) - static let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter()) - static let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter()) - static let contactsService = ContactsService(withContactsAdapter: ContactsAdapter()) + private let daemonService = DaemonService(dRingAdaptor: DRingAdapter()) + private let accountService = AccountsService(withAccountAdapter: AccountAdapter()) + private let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter()) + private let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter()) + private let contactsService = ContactsService(withContactsAdapter: ContactsAdapter()) + + public lazy var injectionBag: InjectionBag = { + return InjectionBag(withDaemonService: self.daemonService, + withAccountService: self.accountService, + withNameService: self.nameService, + withConversationService: self.conversationsService, + withContactsService: self.contactsService) + }() + private lazy var appCoordinator: AppCoordinator = { + return AppCoordinator(with: self.injectionBag) + }() private let log = SwiftyBeaver.self @@ -50,29 +63,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { console.format = "$Dyyyy-MM-dd HH:mm:ss.SSS$d $C$L$c: $M" log.addDestination(console) + // starts the daemon SystemAdapter().registerConfigurationHandler() self.startDaemon() + // themetize the app Chameleon.setGlobalThemeUsingPrimaryColor(UIColor.ringMain, withSecondaryColor: UIColor.ringSecondary, andContentStyle: .light) Chameleon.setRingThemeUsingPrimaryColor(UIColor.ringMain, withSecondaryColor: UIColor.ringSecondary, andContentStyle: .light) - self.loadAccounts() + // load accounts during splashscreen + // and ask the AppCoordinator to handle the first screen once loading is finished + self.accountService.loadAccounts().subscribe { [unowned self] (_) in + if let currentAccount = self.accountService.currentAccount { + self.contactsService.loadContacts(withAccount: currentAccount) + self.contactsService.loadContactRequests(withAccount: currentAccount) + } + self.window?.rootViewController = self.appCoordinator.rootViewController + self.window?.makeKeyAndVisible() + self.appCoordinator.start() + }.disposed(by: self.disposeBag) return true } - func applicationWillResignActive(_ application: UIApplication) { - } - - func applicationDidEnterBackground(_ application: UIApplication) { - } - - func applicationWillEnterForeground(_ application: UIApplication) { - } - - func applicationDidBecomeActive(_ application: UIApplication) { - } - func applicationWillTerminate(_ application: UIApplication) { self.stopDaemon() } @@ -81,7 +94,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fileprivate func startDaemon() { do { - try AppDelegate.daemonService.startDaemon() + try self.daemonService.startDaemon() } catch StartDaemonError.initializationFailure { log.error("Daemon failed to initialize.") } catch StartDaemonError.startFailure { @@ -95,42 +108,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fileprivate func stopDaemon() { do { - try AppDelegate.daemonService.stopDaemon() + try self.daemonService.stopDaemon() } catch StopDaemonError.daemonNotRunning { log.error("Daemon failed to stop because it was not already running.") } catch { log.error("Unknown error in Daemon stop.") } } - - fileprivate func loadAccounts() { - AppDelegate.accountService.loadAccounts() - .subscribe(onSuccess: { (accountList: [AccountModel]) in - self.checkAccount(accountList: accountList) - }, onError: { _ in - self.presentWalkthrough() - }).disposed(by: disposeBag) - } - - fileprivate func checkAccount(accountList: [AccountModel]) { - if accountList.isEmpty { - self.presentWalkthrough() - } else { - AppDelegate.contactsService.loadContacts(withAccount: AppDelegate.accountService.currentAccount!) - AppDelegate.contactsService.loadContactRequests(withAccount: AppDelegate.accountService.currentAccount!) - self.presentMainTabBar() - } - } - - fileprivate func presentWalkthrough() { - let storyboard = UIStoryboard(name: "WalkthroughStoryboard", bundle: nil) - self.window?.rootViewController = storyboard.instantiateInitialViewController() - self.window?.makeKeyAndVisible() - } - - fileprivate func presentMainTabBar() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - self.window?.rootViewController = storyboard.instantiateInitialViewController() - self.window?.makeKeyAndVisible() - } } diff --git a/Ring/Ring/Constants/Generated/Images.swift b/Ring/Ring/Constants/Generated/Images.swift index 8778bbb5fa672ecbf3b9e03dbce62b552d8e13c7..26f50977d4c190039519d4269c9619af15971815 100644 --- a/Ring/Ring/Constants/Generated/Images.swift +++ b/Ring/Ring/Constants/Generated/Images.swift @@ -45,6 +45,7 @@ struct ColorAsset { // swiftlint:disable identifier_name line_length nesting type_body_length type_name enum Asset { + static let backgroundRing = ImageAsset(name: "background_ring") static let icContactPicture = ImageAsset(name: "ic_contact_picture") static let logoRingBeta2Blanc = ImageAsset(name: "logo-ring-beta2-blanc") @@ -52,6 +53,7 @@ enum Asset { static let allColors: [ColorAsset] = [ ] static let allImages: [ImageAsset] = [ + backgroundRing, icContactPicture, logoRingBeta2Blanc, ] diff --git a/Ring/Ring/Constants/Generated/Storyboards.swift b/Ring/Ring/Constants/Generated/Storyboards.swift index b8449a5adf4ddad0b8b02670b7aac9ce7a279e58..aff67e04508c1526f17514c524f2c64753f16b66 100644 --- a/Ring/Ring/Constants/Generated/Storyboards.swift +++ b/Ring/Ring/Constants/Generated/Storyboards.swift @@ -49,38 +49,60 @@ extension UIViewController { // swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name enum StoryboardScene { + enum ContactRequestsViewController: StoryboardType { + static let storyboardName = "ContactRequestsViewController" + + static let initialScene = InitialSceneType<Ring.ContactRequestsViewController>(storyboard: ContactRequestsViewController.self) + } + enum ConversationViewController: StoryboardType { + static let storyboardName = "ConversationViewController" + + static let initialScene = InitialSceneType<Ring.ConversationViewController>(storyboard: ConversationViewController.self) + } + enum CreateAccountViewController: StoryboardType { + static let storyboardName = "CreateAccountViewController" + + static let initialScene = InitialSceneType<Ring.CreateAccountViewController>(storyboard: CreateAccountViewController.self) + } + enum CreateProfileViewController: StoryboardType { + static let storyboardName = "CreateProfileViewController" + + static let initialScene = InitialSceneType<Ring.CreateProfileViewController>(storyboard: CreateProfileViewController.self) + } enum LaunchScreen: StoryboardType { static let storyboardName = "LaunchScreen" static let initialScene = InitialSceneType<UIViewController>(storyboard: LaunchScreen.self) } - enum Main: StoryboardType { - static let storyboardName = "Main" + enum LinkDeviceViewController: StoryboardType { + static let storyboardName = "LinkDeviceViewController" - static let initialScene = InitialSceneType<Ring.MainTabBarViewController>(storyboard: Main.self) + static let initialScene = InitialSceneType<Ring.LinkDeviceViewController>(storyboard: LinkDeviceViewController.self) + } + enum MeDetailViewController: StoryboardType { + static let storyboardName = "MeDetailViewController" - static let mainStoryboard = SceneType<Ring.MainTabBarViewController>(storyboard: Main.self, identifier: "MainStoryboard") + static let initialScene = InitialSceneType<Ring.MeDetailViewController>(storyboard: MeDetailViewController.self) } - enum WalkthroughStoryboard: StoryboardType { - static let storyboardName = "WalkthroughStoryboard" + enum MeViewController: StoryboardType { + static let storyboardName = "MeViewController" - static let initialScene = InitialSceneType<UINavigationController>(storyboard: WalkthroughStoryboard.self) + static let initialScene = InitialSceneType<Ring.MeViewController>(storyboard: MeViewController.self) } -} + enum SmartlistViewController: StoryboardType { + static let storyboardName = "SmartlistViewController" -enum StoryboardSegue { - enum Main: String, SegueType { - case showMessages = "ShowMessages" - case accountDetails + static let initialScene = InitialSceneType<Ring.SmartlistViewController>(storyboard: SmartlistViewController.self) } - enum WalkthroughStoryboard: String, SegueType { - case accountToPermissionsSegue = "AccountToPermissionsSegue" - case createProfileSegue = "CreateProfileSegue" - case linkDeviceToAccountSegue = "LinkDeviceToAccountSegue" - case profileToAccountSegue = "ProfileToAccountSegue" - case profileToLinkSegue = "ProfileToLinkSegue" + enum WelcomeViewController: StoryboardType { + static let storyboardName = "WelcomeViewController" + + static let initialScene = InitialSceneType<Ring.WelcomeViewController>(storyboard: WelcomeViewController.self) } } + +enum StoryboardSegue { +} // swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name private final class BundleToken {} diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift index 459e574236f56d57be1bc6df6c929a26ef51e2f9..3c6b54247780d9878140061faafa150e8da74e41 100644 --- a/Ring/Ring/Constants/Generated/Strings.swift +++ b/Ring/Ring/Constants/Generated/Strings.swift @@ -29,31 +29,38 @@ enum L10n { static let chooseStrongPassword = L10n.tr("Localizable", "createAccount.chooseStrongPassword") /// Create your Ring account static let createAccountFormTitle = L10n.tr("Localizable", "createAccount.createAccountFormTitle") - /// Enter new username + /// username static let enterNewUsernamePlaceholder = L10n.tr("Localizable", "createAccount.enterNewUsernamePlaceholder") - /// Invalid username + /// invalid username static let invalidUsername = L10n.tr("Localizable", "createAccount.invalidUsername") - /// Loading... + /// Loading static let loading = L10n.tr("Localizable", "createAccount.loading") - /// Looking for username availability... + /// looking for username availability static let lookingForUsernameAvailability = L10n.tr("Localizable", "createAccount.lookingForUsernameAvailability") - /// New Password + /// password static let newPasswordPlaceholder = L10n.tr("Localizable", "createAccount.newPasswordPlaceholder") /// 6 characters minimum static let passwordCharactersNumberError = L10n.tr("Localizable", "createAccount.passwordCharactersNumberError") - /// Passwords do not match + /// passwords do not match static let passwordNotMatchingError = L10n.tr("Localizable", "createAccount.passwordNotMatchingError") - /// Register public username (experimental) - static let registerPublicUsername = L10n.tr("Localizable", "createAccount.registerPublicUsername") - /// Repeat new password + /// confirm password static let repeatPasswordPlaceholder = L10n.tr("Localizable", "createAccount.repeatPasswordPlaceholder") - /// Username already taken + /// username already taken static let usernameAlreadyTaken = L10n.tr("Localizable", "createAccount.usernameAlreadyTaken") /// Adding account static let waitCreateAccountTitle = L10n.tr("Localizable", "createAccount.waitCreateAccountTitle") } + enum Createprofile { + /// Skip to Create Account + static let createAccount = L10n.tr("Localizable", "createProfile.createAccount") + /// Skip to Link Device + static let linkDevice = L10n.tr("Localizable", "createProfile.linkDevice") + } + enum Global { + /// Invitations + static let contactRequestsTabBarTitle = L10n.tr("Localizable", "global.contactRequestsTabBarTitle") /// Home static let homeTabBarTitle = L10n.tr("Localizable", "global.homeTabBarTitle") /// Me @@ -79,8 +86,8 @@ enum L10n { /// Create a Ring account static let createAccount = L10n.tr("Localizable", "welcome.createAccount") /// Link this device to an account - static let linkDeviceButton = L10n.tr("Localizable", "welcome.linkDeviceButton") - /// A Ring account allows you to reach people securely in peer to peer through fully distributed network + static let linkDevice = L10n.tr("Localizable", "welcome.linkDevice") + /// Ring is a free and universal communication platform which preserves the users' privacy and freedoms static let text = L10n.tr("Localizable", "welcome.text") /// Welcome to Ring static let title = L10n.tr("Localizable", "welcome.title") diff --git a/Ring/Ring/Coordinators/AppCoordinator.swift b/Ring/Ring/Coordinators/AppCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..b189714c29f9f2fc9902650b1fd3734de65118d4 --- /dev/null +++ b/Ring/Ring/Coordinators/AppCoordinator.swift @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import UIKit +import RxSwift + +/// Represents Application global navigation state +/// +/// - needToOnboard: user has to onboard because he has no account +public enum AppState: State { + case needToOnboard +} + +/// This Coordinator drives the global navigation of the app (presents the UITabBarController + popups the Walkthrough) +class AppCoordinator: Coordinator, StateableResponsive { + + var rootViewController: UIViewController { + return self.tabBarViewController + } + + var childCoordinators = [Coordinator]() + + private let tabBarViewController = UITabBarController() + private let injectionBag: InjectionBag + let disposeBag = DisposeBag() + + let stateSubject = PublishSubject<State>() + + required init (with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + + self.stateSubject.subscribe(onNext: { [unowned self] (state) in + guard let state = state as? AppState else { return } + switch state { + case .needToOnboard: + self.showWalkthrough() + break + } + }).disposed(by: self.disposeBag) + + } + + func start () { + + let conversationsCoordinator = ConversationsCoordinator(with: self.injectionBag) + let contactRequestsCoordinator = ContactRequestsCoordinator(with: self.injectionBag) + let meCoordinator = MeCoordinator(with: self.injectionBag) + + self.tabBarViewController.viewControllers = [conversationsCoordinator.rootViewController, contactRequestsCoordinator.rootViewController, meCoordinator.rootViewController] + self.addChildCoordinator(childCoordinator: conversationsCoordinator) + self.addChildCoordinator(childCoordinator: contactRequestsCoordinator) + self.addChildCoordinator(childCoordinator: meCoordinator) + + self.rootViewController.rx.viewDidAppear.take(1).subscribe(onNext: { [unowned self, unowned conversationsCoordinator, unowned contactRequestsCoordinator, unowned meCoordinator] (_) in + conversationsCoordinator.start() + contactRequestsCoordinator.start() + meCoordinator.start() + + // show walkthrough if needed + if self.injectionBag.accountService.accounts.isEmpty { + self.stateSubject.onNext(AppState.needToOnboard) + } + }).disposed(by: self.disposeBag) + } + + private func showWalkthrough () { + let walkthroughCoordinator = WalkthroughCoordinator(with: self.injectionBag) + self.addChildCoordinator(childCoordinator: walkthroughCoordinator) + let walkthroughViewController = walkthroughCoordinator.rootViewController + self.present(viewController: walkthroughViewController, withStyle: .popup, withAnimation: true) + walkthroughCoordinator.start() + + walkthroughViewController.rx.viewDidDisappear.subscribe(onNext: { [weak self, weak walkthroughCoordinator] (_) in + walkthroughCoordinator?.stateSubject.dispose() + self?.removeChildCoordinator(childCoordinator: walkthroughCoordinator) + }).disposed(by: self.disposeBag) + } +} diff --git a/Ring/Ring/Coordinators/InjectionBag.swift b/Ring/Ring/Coordinators/InjectionBag.swift new file mode 100644 index 0000000000000000000000000000000000000000..10188aad501f2d31afbb21d03f03287654b4c0dd --- /dev/null +++ b/Ring/Ring/Coordinators/InjectionBag.swift @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation + +/// We can centralize in this bag every service that is to be used by every layer of the app +class InjectionBag { + + let daemonService: DaemonService + let accountService: AccountsService + let nameService: NameService + let conversationsService: ConversationsService + let contactsService: ContactsService + + init (withDaemonService daemonService: DaemonService, + withAccountService accountService: AccountsService, + withNameService nameService: NameService, + withConversationService conversationService: ConversationsService, + withContactsService contactsService: ContactsService) { + self.daemonService = daemonService + self.accountService = accountService + self.nameService = nameService + self.conversationsService = conversationService + self.contactsService = contactsService + } + +} diff --git a/Ring/Ring/Extensions/Chameleon+Ring.swift b/Ring/Ring/Extensions/Chameleon+Ring.swift index 7e5710910fa882308db803b96cf48fae09c2cfdd..7ebb62a1685c6ac8f6a6b122305625531548c5f8 100644 --- a/Ring/Ring/Extensions/Chameleon+Ring.swift +++ b/Ring/Ring/Extensions/Chameleon+Ring.swift @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * diff --git a/Ring/Ring/Extensions/NotificationCenter+Ring.swift b/Ring/Ring/Extensions/NotificationCenter+Ring.swift new file mode 100644 index 0000000000000000000000000000000000000000..fbaf310b12a5ff769e561f055e80618e2aa916b6 --- /dev/null +++ b/Ring/Ring/Extensions/NotificationCenter+Ring.swift @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +extension NotificationCenter { + static var keyboardHeight: Observable<CGFloat> { + return Observable + .from([ + NotificationCenter.default.rx.notification(NSNotification.Name.UIKeyboardWillShow) + .map { notification -> CGFloat in + (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0 + }, + NotificationCenter.default.rx.notification(NSNotification.Name.UIKeyboardWillHide) + .map { _ -> CGFloat in + 0 + } + ]) + .merge() + } +} diff --git a/Ring/Ring/Extensions/UIColor+Ring.swift b/Ring/Ring/Extensions/UIColor+Ring.swift index 5d3f88917d873f9352692ce37006448ee63e1360..27d831c548a37dda982a03177d0f04ea147b45c5 100644 --- a/Ring/Ring/Extensions/UIColor+Ring.swift +++ b/Ring/Ring/Extensions/UIColor+Ring.swift @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * diff --git a/Ring/Ring/Extensions/UIView+Ring.swift b/Ring/Ring/Extensions/UIView+Ring.swift index a6a96d286ec8f24dd7029c726babd329c266531c..6fe563b456eb854a9c15819e346896b42bcd0372 100644 --- a/Ring/Ring/Extensions/UIView+Ring.swift +++ b/Ring/Ring/Extensions/UIView+Ring.swift @@ -21,6 +21,11 @@ import Foundation import UIKit +private enum GradientAnchor { + case start + case end +} + extension UIView { @IBInspectable @@ -72,4 +77,71 @@ extension UIView { } } + @IBInspectable + var gradientStartColor: UIColor { + + get { + return self.retrieveGradientColor(for: .start) + } + + set { + self.applyGradientColor(for: .start, with: newValue) + } + } + + @IBInspectable + var gradientEndColor: UIColor { + get { + return self.retrieveGradientColor(for: .end) + } + + set { + self.applyGradientColor(for: .end, with: newValue) + } + } + + private func applyGradientColor(for anchor: GradientAnchor, with color: UIColor) { + if let layer = self.layer.sublayers?[0] as? CAGradientLayer { + // reuse the gradient layer that has already been set + if anchor == .start { + layer.colors = [color.cgColor, self.retrieveGradientColor(for: .end).cgColor] + } else { + layer.colors = [self.retrieveGradientColor(for: .start).cgColor, color.cgColor] + } + return + } + + let layer = CAGradientLayer() + layer.frame = CGRect(origin: .zero, size: self.frame.size) + layer.startPoint = CGPoint(x: 0.5, y: 0) + layer.endPoint = CGPoint(x: 0.5, y: 1) + + if anchor == .start { + layer.colors = [color.cgColor, self.retrieveGradientColor(for: .end).cgColor] + } else { + layer.colors = [self.retrieveGradientColor(for: .start).cgColor, color.cgColor] + } + layer.cornerRadius = self.cornerRadius + + self.layer.addSublayer(layer) + + } + + private func retrieveGradientColor(for anchor: GradientAnchor) -> UIColor { + if let layer = self.layer.sublayers?[0] as? CAGradientLayer, + let colors = layer.colors as? [CGColor] { + if anchor == .start && !colors.isEmpty { + return UIColor(cgColor: colors[0]) + } + + if anchor == .end && colors.count >= 1 { + return UIColor(cgColor: colors[1]) + } + + return UIColor.clear + } + + return UIColor.clear + } + } diff --git a/Ring/Ring/Extensions/UIViewController+Ring.swift b/Ring/Ring/Extensions/UIViewController+Ring.swift new file mode 100644 index 0000000000000000000000000000000000000000..699b6823d9c1087e23af39401324334428461034 --- /dev/null +++ b/Ring/Ring/Extensions/UIViewController+Ring.swift @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import UIKit +import RxSwift + +extension UIViewController { + + /// Find the active UITextField if it exists + /// + /// - Parameters: + /// - view: The UIView to search into + /// - Returns: The active UITextField (ie: isFirstResponder) + func findActiveTextField(in view: UIView) -> UITextField? { + + guard !view.subviews.isEmpty else { return nil } + + for currentView in view.subviews { + if let textfield = currentView as? UITextField, + textfield.isFirstResponder { + return textfield + } + + if let textField = findActiveTextField(in: currentView) { + return textField + } + } + + return nil + } + + /// Scroll the UIScrollView to the right position + /// according to keyboard's height + /// + /// - Parameters: + /// - scrollView: The scrollView to adapt + /// - disposeBag: The RxSwift DisposeBag linked to the UIViewController life cycle + func adaptToKeyboardState (for scrollView: UIScrollView, with disposeBag: DisposeBag) { + + NotificationCenter.keyboardHeight.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self, unowned scrollView] (height) in + let trueHeight = height>0 ? height+100 : 0.0 + let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: trueHeight, right: 0.0) + + scrollView.contentInset = contentInsets + + // If active text field is hidden by keyboard, scroll it so it's visible + // Your app might not need or want this behavior. + if let activeField = self.findActiveTextField(in: scrollView) { + var aRect = self.view.frame + aRect.size.height -= trueHeight + + if !aRect.contains(activeField.frame.origin) { + scrollView.scrollRectToVisible(activeField.frame, animated: true) + } + } + + }).disposed(by: disposeBag) + + } +} diff --git a/Ring/Ring/Extensions/UIViewController+Rx.swift b/Ring/Ring/Extensions/UIViewController+Rx.swift new file mode 100644 index 0000000000000000000000000000000000000000..bfcad388db5e5fc3c1daec9ba6ba4c4ac3097700 --- /dev/null +++ b/Ring/Ring/Extensions/UIViewController+Rx.swift @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import UIKit +import RxSwift +import RxCocoa + +extension Reactive where Base : UIViewController { + public var viewDidLoad: ControlEvent<Void> { + let source = self.sentMessage(#selector(Base.viewDidLoad)).map { _ in } + return ControlEvent(events: source) + } + + public var viewWillAppear: ControlEvent<Bool> { + let source = self.sentMessage(#selector(Base.viewWillAppear)).map { $0.first as? Bool ?? false } + return ControlEvent(events: source) + } + + public var viewDidAppear: ControlEvent<Bool> { + let source = self.sentMessage(#selector(Base.viewDidAppear)).map { $0.first as? Bool ?? false } + return ControlEvent(events: source) + } + + public var viewWillDisappear: ControlEvent<Bool> { + let source = self.sentMessage(#selector(Base.viewWillDisappear)).map { $0.first as? Bool ?? false } + return ControlEvent(events: source) + } + + public var viewDidDisappear: ControlEvent<Bool> { + let source = self.sentMessage(#selector(Base.viewDidDisappear)).map { $0.first as? Bool ?? false } + return ControlEvent(events: source) + } + + public var viewWillLayoutSubviews: ControlEvent<Void> { + let source = self.sentMessage(#selector(Base.viewWillLayoutSubviews)).map { _ in } + return ControlEvent(events: source) + } + + public var viewDidLayoutSubviews: ControlEvent<Void> { + let source = self.sentMessage(#selector(Base.viewDidLayoutSubviews)).map { _ in } + return ControlEvent(events: source) + } + + public var willMoveToParentViewController: ControlEvent<UIViewController?> { + let source = self.sentMessage(#selector(Base.willMove)).map { $0.first as? UIViewController } + return ControlEvent(events: source) + } + public var didMoveToParentViewController: ControlEvent<UIViewController?> { + let source = self.sentMessage(#selector(Base.didMove)).map { $0.first as? UIViewController } + return ControlEvent(events: source) + } + + public var didReceiveMemoryWarning: ControlEvent<Void> { + let source = self.sentMessage(#selector(Base.didReceiveMemoryWarning)).map { _ in } + return ControlEvent(events: source) + } +} diff --git a/Ring/Ring/Contacts/ContactRequestCell.swift b/Ring/Ring/Features/ContactRequests/Cells/ContactRequestCell.swift similarity index 100% rename from Ring/Ring/Contacts/ContactRequestCell.swift rename to Ring/Ring/Features/ContactRequests/Cells/ContactRequestCell.swift diff --git a/Ring/Ring/Contacts/ContactRequestCell.xib b/Ring/Ring/Features/ContactRequests/Cells/ContactRequestCell.xib similarity index 100% rename from Ring/Ring/Contacts/ContactRequestCell.xib rename to Ring/Ring/Features/ContactRequests/Cells/ContactRequestCell.xib diff --git a/Ring/Ring/Contacts/ContactRequestItem.swift b/Ring/Ring/Features/ContactRequests/ContactRequestItem.swift similarity index 100% rename from Ring/Ring/Contacts/ContactRequestItem.swift rename to Ring/Ring/Features/ContactRequests/ContactRequestItem.swift diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestsCoordinator.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..f425344dd0a33ca2c0e0b97b1bfaa68b75a9bfc8 --- /dev/null +++ b/Ring/Ring/Features/ContactRequests/ContactRequestsCoordinator.swift @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +/// This Coordinator drives the Contact Requests navigation +class ContactRequestsCoordinator: Coordinator, StateableResponsive { + + var rootViewController: UIViewController { + return self.navigationViewController + } + + var childCoordinators = [Coordinator]() + + private let navigationViewController = UINavigationController() + private let injectionBag: InjectionBag + let disposeBag = DisposeBag() + + let stateSubject = PublishSubject<State>() + + required init (with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + } + + func start () { + let contactRequestsViewController = ContactRequestsViewController.instantiate(with: self.injectionBag) + self.present(viewController: contactRequestsViewController, withStyle: .show, withAnimation: true) + } +} diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.storyboard b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..4e81aff8b8b9d9f88dfa347deadb9a31fac8d5d2 --- /dev/null +++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.storyboard @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="gUS-mM-rdY"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Invitations--> + <scene sceneID="wW4-22-iqo"> + <objects> + <viewController title="Invitations" id="gUS-mM-rdY" customClass="ContactRequestsViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="DDk-7J-YQx"/> + <viewControllerLayoutGuide type="bottom" id="Y4B-Y3-6GH"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="m1T-eD-duJ"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="bPe-Vn-J7U"> + <rect key="frame" x="0.0" y="20" width="375" height="647"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </tableView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cXe-K6-eM8"> + <rect key="frame" x="0.0" y="20" width="375" height="647"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No invitations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pSV-RV-i1C"> + <rect key="frame" x="135" y="312.5" width="104" height="21"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="pSV-RV-i1C" firstAttribute="centerY" secondItem="cXe-K6-eM8" secondAttribute="centerY" id="ADj-lP-FWa"/> + <constraint firstItem="pSV-RV-i1C" firstAttribute="centerX" secondItem="cXe-K6-eM8" secondAttribute="centerX" id="p8k-zl-EPN"/> + </constraints> + </view> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="Y4B-Y3-6GH" firstAttribute="top" secondItem="bPe-Vn-J7U" secondAttribute="bottom" id="08Z-pN-cbh"/> + <constraint firstAttribute="trailing" secondItem="cXe-K6-eM8" secondAttribute="trailing" id="4kr-6B-acK"/> + <constraint firstItem="cXe-K6-eM8" firstAttribute="leading" secondItem="m1T-eD-duJ" secondAttribute="leading" id="6Ew-hd-hlD"/> + <constraint firstItem="bPe-Vn-J7U" firstAttribute="top" secondItem="DDk-7J-YQx" secondAttribute="bottom" id="EyB-4G-cLb"/> + <constraint firstItem="Y4B-Y3-6GH" firstAttribute="top" secondItem="cXe-K6-eM8" secondAttribute="bottom" id="ZkQ-wy-gvg"/> + <constraint firstAttribute="trailing" secondItem="bPe-Vn-J7U" secondAttribute="trailing" id="eRC-Zj-mtk"/> + <constraint firstItem="cXe-K6-eM8" firstAttribute="top" secondItem="DDk-7J-YQx" secondAttribute="bottom" id="gNJ-Yf-pLu"/> + <constraint firstItem="bPe-Vn-J7U" firstAttribute="leading" secondItem="m1T-eD-duJ" secondAttribute="leading" id="jOx-2Z-yoO"/> + </constraints> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <navigationItem key="navigationItem" id="dMs-kf-EuK"/> + <connections> + <outlet property="noInvitationsPlaceholder" destination="cXe-K6-eM8" id="fuk-gt-pdZ"/> + <outlet property="tableView" destination="bPe-Vn-J7U" id="rc3-OT-FCs"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="SVT-Sr-5RW" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-51" y="351"/> + </scene> + </scenes> +</document> diff --git a/Ring/Ring/Contacts/ContactRequestsViewController.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift similarity index 88% rename from Ring/Ring/Contacts/ContactRequestsViewController.swift rename to Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift index ddcf023fc3bb2d6e68cd02871540a267eb4b49db..b1fbf7f61c44269b2c0580c6906519bbd0aca7ac 100644 --- a/Ring/Ring/Contacts/ContactRequestsViewController.swift +++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift @@ -22,12 +22,11 @@ import UIKit import RxSwift import RxCocoa import SwiftyBeaver +import Reusable -class ContactRequestsViewController: UIViewController { +class ContactRequestsViewController: UIViewController, StoryboardBased, ViewModelBased { - fileprivate let viewModel = ContactRequestsViewModel(withContactsService: AppDelegate.contactsService, - accountsService: AppDelegate.accountService, - nameService: AppDelegate.nameService) + var viewModel: ContactRequestsViewModel! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var noInvitationsPlaceholder: UIView! @@ -38,6 +37,9 @@ class ContactRequestsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + + self.title = L10n.Global.contactRequestsTabBarTitle + self.navigationItem.title = L10n.Global.contactRequestsTabBarTitle } override func viewWillAppear(_ animated: Bool) { @@ -99,8 +101,8 @@ class ContactRequestsViewController: UIViewController { func acceptButtonTapped(withItem item: ContactRequestItem) { viewModel.accept(withItem: item).subscribe(onError: { [unowned self] error in self.log.error("Accept trust request failed") - }, onCompleted: { [unowned self] in - self.log.info("Accept trust request done") + }, onCompleted: { [unowned self] in + self.log.info("Accept trust request done") }).disposed(by: self.disposeBag) } @@ -115,8 +117,8 @@ class ContactRequestsViewController: UIViewController { func banButtonTapped(withItem item: ContactRequestItem) { viewModel.ban(withItem: item).subscribe(onError: { [unowned self] error in self.log.error("Ban trust request failed") - }, onCompleted: { [unowned self] in - self.log.info("Ban trust request done") + }, onCompleted: { [unowned self] in + self.log.info("Ban trust request done") }).disposed(by: self.disposeBag) } } diff --git a/Ring/Ring/Contacts/ContactRequestsViewModel.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift similarity index 89% rename from Ring/Ring/Contacts/ContactRequestsViewModel.swift rename to Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift index e70ac7e02776e95cd00f11cdc5c63f9ba9e02792..dd33af3e2cca0ef49cb368a9909e3b202f4dbdaa 100644 --- a/Ring/Ring/Contacts/ContactRequestsViewModel.swift +++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift @@ -22,7 +22,7 @@ import RxSwift import Contacts import SwiftyBeaver -class ContactRequestsViewModel { +class ContactRequestsViewModel: ViewModel { let contactsService: ContactsService let accountsService: AccountsService @@ -30,12 +30,10 @@ class ContactRequestsViewModel { fileprivate let disposeBag = DisposeBag() - init(withContactsService contactsService: ContactsService, - accountsService: AccountsService, - nameService: NameService) { - self.contactsService = contactsService - self.accountsService = accountsService - self.nameService = nameService + required init(with injectionBag: InjectionBag) { + self.contactsService = injectionBag.contactsService + self.accountsService = injectionBag.accountService + self.nameService = injectionBag.nameService } lazy var contactRequestItems: Observable<[ContactRequestItem]> = { @@ -92,7 +90,7 @@ class ContactRequestsViewModel { fileprivate func lookupUserName(withItem item: ContactRequestItem) { - AppDelegate.nameService.usernameLookupStatus.asObservable() + self.nameService.usernameLookupStatus.asObservable() .filter({ lookupNameResponse in return lookupNameResponse.address == item.contactRequest.ringId }) @@ -105,7 +103,7 @@ class ContactRequestsViewModel { }) .disposed(by: self.disposeBag) - AppDelegate.nameService.lookupAddress(withAccount: (accountsService.currentAccount?.id)!, + self.nameService.lookupAddress(withAccount: (accountsService.currentAccount?.id)!, nameserver: "", address: item.contactRequest.ringId) } diff --git a/Ring/Ring/Messages/MessageCellReceived.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.swift similarity index 100% rename from Ring/Ring/Messages/MessageCellReceived.swift rename to Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.swift diff --git a/Ring/Ring/Messages/MessageCellReceived.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib similarity index 100% rename from Ring/Ring/Messages/MessageCellReceived.xib rename to Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib diff --git a/Ring/Ring/Messages/MessageCellSent.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.swift similarity index 100% rename from Ring/Ring/Messages/MessageCellSent.swift rename to Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.swift diff --git a/Ring/Ring/Messages/MessageCellSent.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib similarity index 96% rename from Ring/Ring/Messages/MessageCellSent.xib rename to Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib index 5fe0dd295d89b67244340e9afdceab464f7afcd0..04dc0efe4a881bf121603ea92110f882db6576f1 100644 --- a/Ring/Ring/Messages/MessageCellSent.xib +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> <device id="retina4_7" orientation="portrait"> <adaptation id="fullscreen"/> </device> diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.storyboard b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..0f85f8eb3f84a18b1d811d82292d69d3d02e71b3 --- /dev/null +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.storyboard @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="O1m-sW-gim"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Conversation View Controller--> + <scene sceneID="2Fj-m2-pCD"> + <objects> + <viewController hidesBottomBarWhenPushed="YES" id="O1m-sW-gim" customClass="ConversationViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="Hdp-cb-K7b"/> + <viewControllerLayoutGuide type="bottom" id="zju-EL-lSj"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="lhx-ny-Zct"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="yc2-Jn-6vm"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <prototypes> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MessageCell" id="Cd7-Fm-IM5"> + <rect key="frame" x="0.0" y="28" width="375" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Cd7-Fm-IM5" id="TOb-Hu-RG9"> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + </tableViewCellContentView> + </tableViewCell> + </prototypes> + </tableView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Wq-EJ-CAF"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <subviews> + <activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="NYW-Ie-8yB"> + <rect key="frame" x="169.5" y="315" width="37" height="37"/> + <color key="color" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> + </activityIndicatorView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="NYW-Ie-8yB" firstAttribute="centerY" secondItem="6Wq-EJ-CAF" secondAttribute="centerY" id="I23-W6-yIz"/> + <constraint firstItem="NYW-Ie-8yB" firstAttribute="centerX" secondItem="6Wq-EJ-CAF" secondAttribute="centerX" id="vB4-hR-9sj"/> + </constraints> + </view> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="6Wq-EJ-CAF" firstAttribute="leading" secondItem="lhx-ny-Zct" secondAttribute="leading" id="5VA-aR-jIL"/> + <constraint firstAttribute="trailing" secondItem="yc2-Jn-6vm" secondAttribute="trailing" id="6Ar-yh-iTT"/> + <constraint firstAttribute="bottom" secondItem="6Wq-EJ-CAF" secondAttribute="bottom" id="QKw-Wp-ff0"/> + <constraint firstItem="yc2-Jn-6vm" firstAttribute="top" secondItem="lhx-ny-Zct" secondAttribute="top" id="Qd0-eb-gyZ"/> + <constraint firstAttribute="trailing" secondItem="6Wq-EJ-CAF" secondAttribute="trailing" id="W84-gc-kua"/> + <constraint firstItem="yc2-Jn-6vm" firstAttribute="leading" secondItem="lhx-ny-Zct" secondAttribute="leading" id="Wzk-uM-Oge"/> + <constraint firstAttribute="bottom" secondItem="yc2-Jn-6vm" secondAttribute="bottom" id="m6U-Gp-jhl"/> + <constraint firstItem="6Wq-EJ-CAF" firstAttribute="top" secondItem="lhx-ny-Zct" secondAttribute="top" id="v3Q-NK-vb1"/> + </constraints> + </view> + <extendedEdge key="edgesForExtendedLayout"/> + <connections> + <outlet property="spinnerView" destination="6Wq-EJ-CAF" id="XKK-Rh-rmd"/> + <outlet property="tableView" destination="yc2-Jn-6vm" id="neM-4t-MpC"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="lOF-r3-fSY" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="844" y="-1179"/> + </scene> + </scenes> +</document> diff --git a/Ring/Ring/Conversations/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift similarity index 92% rename from Ring/Ring/Conversations/ConversationViewController.swift rename to Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift index 79d7ae86a2fde25087eb240f090cf85b82d336c4..cc83f7244340148cbbf2655d7f0d001413d2217b 100644 --- a/Ring/Ring/Conversations/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -20,19 +20,20 @@ import UIKit import RxSwift +import Reusable -class ConversationViewController: UIViewController, UITextFieldDelegate { +class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased { + + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var spinnerView: UIView! let disposeBag = DisposeBag() - var viewModel: ConversationViewModel? + var viewModel: ConversationViewModel! var messageViewModels: [MessageViewModel]? var textFieldShouldEndEditing = false var bottomOffset: CGFloat = 0 - @IBOutlet weak var tableView: UITableView! - @IBOutlet weak var spinnerView: UIView! - override func viewDidLoad() { super.viewDidLoad() @@ -72,7 +73,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate { } func setupUI() { - self.viewModel?.userName.asObservable().bind(to: self.navigationItem.rx.title).disposed(by: disposeBag) + self.viewModel.userName.asObservable().bind(to: self.navigationItem.rx.title).disposed(by: disposeBag) self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height @@ -89,7 +90,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate { super.viewWillDisappear(animated) self.textFieldShouldEndEditing = true - self.viewModel?.setMessagesAsRead() + self.viewModel.setMessagesAsRead() } func setupTableView() { @@ -104,13 +105,13 @@ class ConversationViewController: UIViewController, UITextFieldDelegate { self.tableView.register(cellType: MessageCellReceived.self) //Bind the TableView to the ViewModel - self.viewModel?.messages.subscribe(onNext: { [weak self] (messageViewModels) in + self.viewModel.messages.subscribe(onNext: { [weak self] (messageViewModels) in self?.messageViewModels = messageViewModels self?.tableView.reloadData() }).disposed(by: self.disposeBag) //Scroll to bottom when reloaded - self.tableView.rx.methodInvoked(#selector(UITableView.reloadData)).subscribe(onNext: { _ in + self.tableView.rx.methodInvoked(#selector(UITableView.reloadData)).subscribe(onNext: { [unowned self] _ in self.scrollToBottomIfNeed() self.updateBottomOffset() }).disposed(by: disposeBag) @@ -160,8 +161,8 @@ class ConversationViewController: UIViewController, UITextFieldDelegate { func setupBindings() { //Binds the keyboard Send button action to the ViewModel - self.messageAccessoryView.messageTextField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { _ in - self.viewModel?.sendMessage(withContent: self.messageAccessoryView.messageTextField.text!) + self.messageAccessoryView.messageTextField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { [unowned self] _ in + self.viewModel.sendMessage(withContent: self.messageAccessoryView.messageTextField.text!) self.messageAccessoryView.messageTextField.text = "" }).disposed(by: disposeBag) } diff --git a/Ring/Ring/Conversations/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift similarity index 65% rename from Ring/Ring/Conversations/ConversationViewModel.swift rename to Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index 30eddb749ec3452fe7fb90da1549366b4da8d5d3..38a72c5cd48d1ae0405c5db211f8a3217df582d9 100644 --- a/Ring/Ring/Conversations/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -23,14 +23,58 @@ import RxSwift import RealmSwift import SwiftyBeaver -class ConversationViewModel { +class ConversationViewModel: ViewModel { /** logguer */ private let log = SwiftyBeaver.self - let conversation: ConversationModel + var conversation: ConversationModel! { + didSet { + //Create observable from sorted conversations and flatMap them to view models + self.messages = self.conversationsService.conversations.map({ [unowned self] conversations in + return conversations.filter({ conv in + let recipient1 = conv.recipientRingId + let recipient2 = self.conversation.recipientRingId + if recipient1 == recipient2 { + return true + } + return false + }).flatMap({ conversation in + conversation.messages.map({ [unowned self] message in + return MessageViewModel(withInjectionBag: self.injectionBag, withMessage: message) + }) + }) + }).observeOn(MainScheduler.instance) + + let contact = self.contactsService.contact(withRingId: self.conversation.recipientRingId) + + if let contactUserName = contact?.userName { + self.userName.onNext(contactUserName) + } else { + + let recipientRingId = self.conversation.recipientRingId + + //Return an observer for the username lookup + self.nameService.usernameLookupStatus + .filter({ lookupNameResponse in + return lookupNameResponse.address != nil && + lookupNameResponse.address == recipientRingId + }).subscribe(onNext: { [unowned self] lookupNameResponse in + if let name = lookupNameResponse.name { + self.userName.onNext(name) + contact?.userName = name + } else if let address = lookupNameResponse.address { + self.userName.onNext(address) + } + }).disposed(by: disposeBag) + + self.nameService.lookupAddress(withAccount: "", nameserver: "", address: self.conversation.recipientRingId) + } + } + } + private lazy var realm: Realm = { guard let realm = try? Realm() else { fatalError("Enable to instantiate Realm") @@ -47,56 +91,26 @@ class ConversationViewModel { private let disposeBag = DisposeBag() - let messages: Observable<[MessageViewModel]> + var messages: Observable<[MessageViewModel]>! var userName = BehaviorSubject(value: "") //Services - private let conversationsService = AppDelegate.conversationsService - private let accountService = AppDelegate.accountService - private let contactsService = AppDelegate.contactsService - - init(withConversation conversation: ConversationModel) { - self.conversation = conversation + private let conversationsService: ConversationsService + private let accountService: AccountsService + private let nameService: NameService + private let contactsService: ContactsService + private let injectionBag: InjectionBag + + required init(with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + self.accountService = injectionBag.accountService + self.conversationsService = injectionBag.conversationsService + self.nameService = injectionBag.nameService + self.contactsService = injectionBag.contactsService dateFormatter.dateStyle = .medium hourFormatter.dateFormat = "HH:mm" - - //Create observable from sorted conversations and flatMap them to view models - self.messages = self.conversationsService.conversations.map({ conversations in - return conversations.filter({ conv in - return conv.isEqual(conversation) - }).flatMap({ conversation in - conversation.messages.map({ message in - return MessageViewModel(withMessage: message) - }) - }) - }).observeOn(MainScheduler.instance) - - let contact = contactsService.contact(withRingId: self.conversation.recipientRingId) - - if let contactUserName = contact?.userName { - userName.onNext(contactUserName) - } else { - - let recipientRingId = self.conversation.recipientRingId - - //Return an observer for the username lookup - AppDelegate.nameService.usernameLookupStatus - .filter({ lookupNameResponse in - return lookupNameResponse.address != nil && - lookupNameResponse.address == recipientRingId - }).subscribe(onNext: { [unowned self] lookupNameResponse in - if let name = lookupNameResponse.name { - self.userName.onNext(name) - contact?.userName = name - } else if let address = lookupNameResponse.address { - self.userName.onNext(address) - } - }).disposed(by: disposeBag) - - AppDelegate.nameService.lookupAddress(withAccount: "", nameserver: "", address: self.conversation.recipientRingId) - } } var unreadMessages: String { diff --git a/Ring/Ring/Messages/MessageAccessoryView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageAccessoryView.swift similarity index 100% rename from Ring/Ring/Messages/MessageAccessoryView.swift rename to Ring/Ring/Features/Conversations/Conversation/MessageAccessoryView.swift diff --git a/Ring/Ring/Messages/MessageAccessoryView.xib b/Ring/Ring/Features/Conversations/Conversation/MessageAccessoryView.xib similarity index 100% rename from Ring/Ring/Messages/MessageAccessoryView.xib rename to Ring/Ring/Features/Conversations/Conversation/MessageAccessoryView.xib diff --git a/Ring/Ring/Messages/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift similarity index 86% rename from Ring/Ring/Messages/MessageViewModel.swift rename to Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift index 927daaf894fa1b3203fc1eb04730da714cf4e76a..5843907308b8bb41005cee8c62472bd7de3aa253 100644 --- a/Ring/Ring/Messages/MessageViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift @@ -27,10 +27,12 @@ enum BubblePosition { class MessageViewModel { - fileprivate let accountService = AppDelegate.accountService + fileprivate let accountService: AccountsService fileprivate var message: MessageModel - init(withMessage message: MessageModel) { + init(withInjectionBag injectionBag: InjectionBag, + withMessage message: MessageModel) { + self.accountService = injectionBag.accountService self.message = message } diff --git a/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..21499653c297003277a75114431e15612697d795 --- /dev/null +++ b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +/// Represents Conversations navigation state +/// +/// - conversationDetail: user want to see a conversation detail +enum ConversationsState: State { + case conversationDetail(conversationViewModel: ConversationViewModel) +} + +/// This Coordinator drives the conversation navigation (Smartlist / Conversation detail) +class ConversationsCoordinator: Coordinator, StateableResponsive { + + var rootViewController: UIViewController { + return self.navigationViewController + } + + var childCoordinators = [Coordinator]() + + private let navigationViewController = UINavigationController() + private let injectionBag: InjectionBag + let disposeBag = DisposeBag() + + let stateSubject = PublishSubject<State>() + + required init (with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + + self.stateSubject.subscribe(onNext: { [unowned self] (state) in + guard let state = state as? ConversationsState else { return } + switch state { + case .conversationDetail (let conversationViewModel): + self.showConversation(withConversationViewModel: conversationViewModel) + break + } + }).disposed(by: self.disposeBag) + + } + + func start () { + let smartListViewController = SmartlistViewController.instantiate(with: self.injectionBag) + self.present(viewController: smartListViewController, withStyle: .show, withAnimation: true, withStateable: smartListViewController.viewModel) + } + + private func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) { + let conversationViewController = ConversationViewController.instantiate(with: self.injectionBag) + conversationViewController.viewModel.conversation = conversationViewModel.conversation + self.present(viewController: conversationViewController, withStyle: .show, withAnimation: true) + } +} diff --git a/Ring/Ring/Conversations/ConversationCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift similarity index 100% rename from Ring/Ring/Conversations/ConversationCell.swift rename to Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift diff --git a/Ring/Ring/Conversations/ConversationCell.xib b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib similarity index 100% rename from Ring/Ring/Conversations/ConversationCell.xib rename to Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib diff --git a/Ring/Ring/Conversations/ConversationSection.swift b/Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift similarity index 100% rename from Ring/Ring/Conversations/ConversationSection.swift rename to Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..2eab0160c3be631b39d6d4741eb9e8d61ddee840 --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Raw-Ee-7sK"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Home--> + <scene sceneID="oD9-xn-mt7"> + <objects> + <viewController title="Home" id="Raw-Ee-7sK" customClass="SmartlistViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="sbJ-yn-t3e"/> + <viewControllerLayoutGuide type="bottom" id="cfq-zl-uux"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="2dZ-8A-4nq"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <searchBar contentMode="redraw" placeholder="Enter name..." translatesAutoresizingMaskIntoConstraints="NO" id="xPr-nI-I35"> + <rect key="frame" x="0.0" y="20" width="375" height="44"/> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <color key="tintColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> + <offsetWrapper key="searchFieldBackgroundPositionAdjustment" horizontal="0.0" vertical="0.0"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="namePhonePad" returnKeyType="done"/> + <connections> + <outlet property="delegate" destination="Raw-Ee-7sK" id="Gew-Uj-8Rz"/> + </connections> + </searchBar> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HFM-G6-hMN"> + <rect key="frame" x="0.0" y="64" width="375" height="603"/> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </tableView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EvL-Bu-O1T"> + <rect key="frame" x="0.0" y="64" width="375" height="603"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No conversations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8bB-XU-6gh"> + <rect key="frame" x="121.5" y="291" width="133" height="21"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="8bB-XU-6gh" firstAttribute="centerY" secondItem="EvL-Bu-O1T" secondAttribute="centerY" id="PQg-Np-JN3"/> + <constraint firstItem="8bB-XU-6gh" firstAttribute="centerX" secondItem="EvL-Bu-O1T" secondAttribute="centerX" id="gY7-3l-mS7"/> + </constraints> + </view> + <tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="opE-y7-3Rm"> + <rect key="frame" x="0.0" y="64" width="375" height="603"/> + <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> + <label key="tableHeaderView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="4Yu-Fe-ixq"> + <rect key="frame" x="0.0" y="0.0" width="375" height="24"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + </tableView> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="opE-y7-3Rm" firstAttribute="leading" secondItem="2dZ-8A-4nq" secondAttribute="leading" id="0cn-0y-g9L"/> + <constraint firstItem="EvL-Bu-O1T" firstAttribute="leading" secondItem="2dZ-8A-4nq" secondAttribute="leading" id="2Cw-xT-xCg"/> + <constraint firstItem="xPr-nI-I35" firstAttribute="leading" secondItem="2dZ-8A-4nq" secondAttribute="leading" id="4NO-53-KBh"/> + <constraint firstAttribute="trailing" secondItem="HFM-G6-hMN" secondAttribute="trailing" id="7H1-ka-2Ti"/> + <constraint firstItem="HFM-G6-hMN" firstAttribute="leading" secondItem="2dZ-8A-4nq" secondAttribute="leading" id="8tr-Rd-1kr"/> + <constraint firstItem="HFM-G6-hMN" firstAttribute="top" secondItem="xPr-nI-I35" secondAttribute="bottom" id="9lS-x7-SaG"/> + <constraint firstAttribute="trailing" secondItem="opE-y7-3Rm" secondAttribute="trailing" id="D0f-nM-KEs"/> + <constraint firstItem="xPr-nI-I35" firstAttribute="top" secondItem="sbJ-yn-t3e" secondAttribute="bottom" id="OX7-et-d3F"/> + <constraint firstItem="cfq-zl-uux" firstAttribute="top" secondItem="EvL-Bu-O1T" secondAttribute="bottom" id="TsM-9H-eI1"/> + <constraint firstItem="cfq-zl-uux" firstAttribute="top" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="VfB-5H-uHq"/> + <constraint firstItem="opE-y7-3Rm" firstAttribute="top" secondItem="xPr-nI-I35" secondAttribute="bottom" id="aeg-VV-nOD"/> + <constraint firstAttribute="trailing" secondItem="xPr-nI-I35" secondAttribute="trailing" id="dtX-d4-r9P"/> + <constraint firstItem="EvL-Bu-O1T" firstAttribute="top" secondItem="xPr-nI-I35" secondAttribute="bottom" id="izx-u2-l6g"/> + <constraint firstAttribute="trailing" secondItem="EvL-Bu-O1T" secondAttribute="trailing" id="nSK-QH-snj"/> + <constraint firstItem="cfq-zl-uux" firstAttribute="top" secondItem="opE-y7-3Rm" secondAttribute="bottom" id="wun-hC-1JP"/> + </constraints> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <tabBarItem key="tabBarItem" title="Home" id="o5c-No-Dpq"/> + <navigationItem key="navigationItem" id="zLl-0A-Dht"/> + <connections> + <outlet property="conversationsTableView" destination="HFM-G6-hMN" id="M97-IB-NUZ"/> + <outlet property="noConversationsView" destination="EvL-Bu-O1T" id="tVV-6a-4Xg"/> + <outlet property="searchBar" destination="xPr-nI-I35" id="Y3U-rV-yfc"/> + <outlet property="searchResultsTableView" destination="opE-y7-3Rm" id="F3g-9d-IQt"/> + <outlet property="searchTableViewLabel" destination="4Yu-Fe-ixq" id="dq3-QM-bfA"/> + <outlet property="tableView" destination="HFM-G6-hMN" id="Gci-vk-ijr"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="JSt-CJ-9Vq" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-97.5" y="-1177.8169014084508"/> + </scene> + </scenes> +</document> diff --git a/Ring/Ring/Smartlist/SmartlistViewController.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift similarity index 81% rename from Ring/Ring/Smartlist/SmartlistViewController.swift rename to Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift index b881e7d5bd78e9d6eeda043dfa92e90e00379f27..a56fd6f0b0ace4c8f16cae9236b1569ce22320b4 100644 --- a/Ring/Ring/Smartlist/SmartlistViewController.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift @@ -25,33 +25,28 @@ import RxCocoa import Reusable //Constants -fileprivate let showMessages = "ShowMessages" - -fileprivate let smartlistRowHeight: CGFloat = 64.0 -fileprivate let tableHeaderViewHeight: CGFloat = 24.0 -fileprivate let firstSectionHeightForHeader: CGFloat = 31.0 //Compensate the offset due to the label on the top of the tableView -fileprivate let defaultSectionHeightForHeader: CGFloat = 55.0 +fileprivate struct SmartlistConstants { + static let smartlistRowHeight: CGFloat = 64.0 + static let tableHeaderViewHeight: CGFloat = 24.0 + static let firstSectionHeightForHeader: CGFloat = 31.0 //Compensate the offset due to the label on the top of the tableView + static let defaultSectionHeightForHeader: CGFloat = 55.0 +} -class SmartlistViewController: UIViewController { +class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased { + // MARK: outlets @IBOutlet weak var tableView: UITableView! - - fileprivate let viewModel = SmartlistViewModel(withConversationsService: AppDelegate.conversationsService, - nameService: AppDelegate.nameService, - accountsService: AppDelegate.accountService, - contactsService: AppDelegate.contactsService) - @IBOutlet weak var conversationsTableView: UITableView! @IBOutlet weak var searchResultsTableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var noConversationsView: UIView! @IBOutlet weak var searchTableViewLabel: UILabel! + // MARK: members + var viewModel: SmartlistViewModel! fileprivate let disposeBag = DisposeBag() - //ConverationViewModel to be passed to the Messages screen - fileprivate var selectedItem: ConversationViewModel? - + // MARK: functions override func viewDidLoad() { super.viewDidLoad() @@ -80,10 +75,6 @@ class SmartlistViewController: UIViewController { self.navigationItem.rightBarButtonItem = self.editButtonItem } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) self.conversationsTableView.setEditing(editing, animated: true) @@ -120,9 +111,9 @@ class SmartlistViewController: UIViewController { let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, ConversationSection.Item) -> UITableViewCell = { ( dataSource: TableViewSectionedDataSource<ConversationSection>, - tableView: UITableView, - indexPath: IndexPath, - item: ConversationSection.Item) in + tableView: UITableView, + indexPath: IndexPath, + item: ConversationSection.Item) in let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ConversationCell.self) @@ -161,28 +152,28 @@ class SmartlistViewController: UIViewController { func setupTableViews() { //Set row height - self.conversationsTableView.rowHeight = smartlistRowHeight - self.searchResultsTableView.rowHeight = smartlistRowHeight + self.conversationsTableView.rowHeight = SmartlistConstants.smartlistRowHeight + self.searchResultsTableView.rowHeight = SmartlistConstants.smartlistRowHeight //Register Cell self.conversationsTableView.register(cellType: ConversationCell.self) self.searchResultsTableView.register(cellType: ConversationCell.self) //Bind to ViewModel to show or hide the filtered results - self.viewModel.isSearching.subscribe(onNext: { isSearching in + self.viewModel.isSearching.subscribe(onNext: { [unowned self] (isSearching) in self.searchResultsTableView.isHidden = !isSearching }).disposed(by: disposeBag) //Show the Messages screens and pass the viewModel for Conversations - self.conversationsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { item in - self.selectedItem = item - self.performSegue(withIdentifier: showMessages, sender: nil) + self.conversationsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { [unowned self] item in + self.cancelSearch() + self.viewModel.showConversation(withConversationViewModel: item) }).disposed(by: disposeBag) //Show the Messages screens and pass the viewModel for Search Results - self.searchResultsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { item in - self.selectedItem = item - self.performSegue(withIdentifier: showMessages, sender: nil) + self.searchResultsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { [unowned self] item in + self.cancelSearch() + self.viewModel.showConversation(withConversationViewModel: item) }).disposed(by: disposeBag) //Deselect the rows @@ -247,17 +238,6 @@ class SmartlistViewController: UIViewController { self.searchResultsTableView.isHidden = true } - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let msgVC = segue.destination as? ConversationViewController { - self.cancelSearch() - self.viewModel.selected(item: self.selectedItem!) - msgVC.viewModel = self.selectedItem - } - } - } extension SmartlistViewController : UITableViewDelegate { @@ -265,9 +245,9 @@ extension SmartlistViewController : UITableViewDelegate { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { if section == 0 { - return firstSectionHeightForHeader + return SmartlistConstants.firstSectionHeightForHeader } else { - return defaultSectionHeightForHeader + return SmartlistConstants.defaultSectionHeightForHeader } } } diff --git a/Ring/Ring/Smartlist/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift similarity index 88% rename from Ring/Ring/Smartlist/SmartlistViewModel.swift rename to Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift index 190604f169aae0a8ae975dfc07941ebab93d5f83..9ef2b93b39fe8639c35c73ab0de56a5d21a3443e 100644 --- a/Ring/Ring/Smartlist/SmartlistViewModel.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift @@ -20,13 +20,20 @@ import RxSwift -class SmartlistViewModel { +class SmartlistViewModel: Stateable, ViewModel { + + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() fileprivate let disposeBag = DisposeBag() //Services fileprivate let conversationsService: ConversationsService fileprivate let nameService: NameService + fileprivate let accountsService: AccountsService fileprivate let contactsService: ContactsService let searchBarText = Variable<String>("") @@ -40,12 +47,12 @@ class SmartlistViewModel { fileprivate var contactFoundConversation = Variable<ConversationViewModel?>(nil) fileprivate var conversationViewModels = [ConversationViewModel]() - init(withConversationsService conversationsService: ConversationsService, nameService: NameService, - accountsService: AccountsService, contactsService: ContactsService) { + required init(with injectionBag: InjectionBag) { - self.conversationsService = conversationsService - self.nameService = nameService - self.contactsService = contactsService + self.conversationsService = injectionBag.conversationsService + self.nameService = injectionBag.nameService + self.accountsService = injectionBag.accountService + self.contactsService = injectionBag.contactsService //Create observable from sorted conversations and flatMap them to view models let conversationsObservable: Observable<[ConversationViewModel]> = self.conversationsService.conversations.asObservable().map({ conversations in @@ -67,7 +74,8 @@ class SmartlistViewModel { }).first { conversationViewModel = foundConversationViewModel } else { - conversationViewModel = ConversationViewModel(withConversation: conversationModel) + conversationViewModel = ConversationViewModel(with: injectionBag) + conversationViewModel?.conversation = conversationModel self.conversationViewModels.append(conversationViewModel!) } @@ -114,8 +122,9 @@ class SmartlistViewModel { }).disposed(by: disposeBag) //Observe username lookup - self.nameService.usernameLookupStatus.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self] usernameLookupStatus in + self.nameService.usernameLookupStatus.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self, unowned injectionBag] usernameLookupStatus in if usernameLookupStatus.state == .found && usernameLookupStatus.name == self.searchBarText.value { + if let conversation = self.conversationViewModels.filter({ conversationViewModel in conversationViewModel.conversation.recipientRingId == usernameLookupStatus.address }).first { @@ -126,7 +135,8 @@ class SmartlistViewModel { //Create new converation let conversation = ConversationModel(withRecipientRingId: usernameLookupStatus.address, accountId: "") - let newConversation = ConversationViewModel(withConversation: conversation) + let newConversation = ConversationViewModel(with: injectionBag) + newConversation.conversation = conversation self.contactFoundConversation.value = newConversation } } @@ -192,4 +202,8 @@ class SmartlistViewModel { self.conversationViewModels.remove(at: index) } } + + func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) { + self.stateSubject.onNext(ConversationsState.conversationDetail(conversationViewModel: conversationViewModel)) + } } diff --git a/Ring/Ring/Features/Me/Detail/MeDetailViewController.storyboard b/Ring/Ring/Features/Me/Detail/MeDetailViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..291ff69e0444f3de26164eb67a3c1339efc33609 --- /dev/null +++ b/Ring/Ring/Features/Me/Detail/MeDetailViewController.storyboard @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Xnm-t3-8pO"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Details--> + <scene sceneID="Yh1-8d-nle"> + <objects> + <viewController title="Details" id="Xnm-t3-8pO" customClass="MeDetailViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="C4W-cD-ScH"/> + <viewControllerLayoutGuide type="bottom" id="gIx-Me-F8R"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="mBS-Fy-f7e"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="clip" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="YiK-C6-85o"> + <rect key="frame" x="16" y="40" width="343" height="16"/> + <fontDescription key="fontDescription" type="system" pointSize="13"/> + <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="YiK-C6-85o" firstAttribute="top" secondItem="C4W-cD-ScH" secondAttribute="bottom" constant="20" id="0qe-xX-bzR"/> + <constraint firstItem="YiK-C6-85o" firstAttribute="leading" secondItem="mBS-Fy-f7e" secondAttribute="leadingMargin" id="mvb-LU-BZ8"/> + <constraint firstItem="YiK-C6-85o" firstAttribute="trailing" secondItem="mBS-Fy-f7e" secondAttribute="trailingMargin" id="oie-oE-20L"/> + </constraints> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <navigationItem key="navigationItem" title="Details" id="QbY-2p-rda"/> + <connections> + <outlet property="detailsLabel" destination="YiK-C6-85o" id="Vu7-1P-P7p"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="UcU-7l-x8f" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="784" y="-438"/> + </scene> + </scenes> +</document> diff --git a/Ring/Ring/Settings/AccountDetailsViewController.swift b/Ring/Ring/Features/Me/Detail/MeDetailViewController.swift similarity index 87% rename from Ring/Ring/Settings/AccountDetailsViewController.swift rename to Ring/Ring/Features/Me/Detail/MeDetailViewController.swift index 3be87e912075e1b5dd5240ed533d42d35b05fc41..dd2c67cd6d56abe549f3ceddef2aca9709499fb4 100644 --- a/Ring/Ring/Settings/AccountDetailsViewController.swift +++ b/Ring/Ring/Features/Me/Detail/MeDetailViewController.swift @@ -19,11 +19,13 @@ */ import UIKit +import Reusable -class AccountDetailsViewController: UIViewController { +class MeDetailViewController: UIViewController, StoryboardBased, ViewModelBased { // MARK: - Properties var account: AccountModel! + var viewModel: MeDetailViewModel! @IBOutlet weak var detailsLabel: UILabel! diff --git a/Ring/Ring/Walkthrough/LinkDeviceToAccountViewController.swift b/Ring/Ring/Features/Me/Detail/MeDetailViewModel.swift similarity index 79% rename from Ring/Ring/Walkthrough/LinkDeviceToAccountViewController.swift rename to Ring/Ring/Features/Me/Detail/MeDetailViewModel.swift index 0ba158f6a166cfd0f17441bf63a23348fd5231c7..1aea0c32875629e589aed82e84ccfec13851c2b8 100644 --- a/Ring/Ring/Walkthrough/LinkDeviceToAccountViewController.swift +++ b/Ring/Ring/Features/Me/Detail/MeDetailViewModel.swift @@ -1,7 +1,7 @@ /* * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Silbino Goncalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,12 +18,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import UIKit +import Foundation -class LinkDeviceToAccountViewController: UIViewController { +class MeDetailViewModel: ViewModel { - override func viewDidLoad() { - super.viewDidLoad() + required init(with injectionBag: InjectionBag) { } } diff --git a/Ring/Ring/Account/AccountTableViewCell.swift b/Ring/Ring/Features/Me/Me/Cells/AccountTableViewCell.swift similarity index 100% rename from Ring/Ring/Account/AccountTableViewCell.swift rename to Ring/Ring/Features/Me/Me/Cells/AccountTableViewCell.swift diff --git a/Ring/Ring/Features/Me/Me/MeViewController.storyboard b/Ring/Ring/Features/Me/Me/MeViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..80a1ae9582f8ab30291189042d6608f5777e8ab2 --- /dev/null +++ b/Ring/Ring/Features/Me/Me/MeViewController.storyboard @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="RuW-kz-iBP"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Me--> + <scene sceneID="JC9-vU-UJ4"> + <objects> + <viewController title="Me" id="RuW-kz-iBP" customClass="MeViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="SYf-gf-IKh"/> + <viewControllerLayoutGuide type="bottom" id="tam-QD-Xpf"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="nkh-py-Uar"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="pZo-fN-8Fj"> + <rect key="frame" x="132.5" y="297" width="110" height="110"/> + <constraints> + <constraint firstAttribute="width" constant="110" id="Nps-H6-gl2"/> + <constraint firstAttribute="height" constant="110" id="yhs-CD-gXk"/> + </constraints> + </imageView> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="XF9-LL-io7"> + <rect key="frame" x="0.0" y="449" width="375" height="218"/> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="height" constant="218" id="VQN-HR-xT0"/> + </constraints> + <prototypes> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="AccountTableViewCell" id="HQc-Zx-7l9" customClass="AccountTableViewCell" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="28" width="375" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="HQc-Zx-7l9" id="KOa-Yg-Sgf"> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ljw-sY-kbY"> + <rect key="frame" x="8" y="6" width="51" height="31"/> + <connections> + <action selector="switchAccountState:" destination="HQc-Zx-7l9" eventType="valueChanged" id="UD8-Qj-NgC"/> + </connections> + </switch> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TYPE" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zmt-fY-s9P"> + <rect key="frame" x="230" y="11" width="42" height="21"/> + <constraints> + <constraint firstAttribute="width" constant="42" id="kXz-v3-xRs"/> + </constraints> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pEo-sN-P32" userLabel="Account Name Label"> + <rect key="frame" x="71" y="11" width="100" height="21"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstItem="Zmt-fY-s9P" firstAttribute="baseline" secondItem="pEo-sN-P32" secondAttribute="baseline" id="72G-HL-GXC"/> + <constraint firstItem="Zmt-fY-s9P" firstAttribute="leading" secondItem="pEo-sN-P32" secondAttribute="trailing" constant="59" id="Agl-eH-BKo"/> + <constraint firstItem="ljw-sY-kbY" firstAttribute="centerY" secondItem="KOa-Yg-Sgf" secondAttribute="centerY" id="Oda-Wx-tbm"/> + <constraint firstItem="ljw-sY-kbY" firstAttribute="leading" secondItem="KOa-Yg-Sgf" secondAttribute="leadingMargin" id="PUS-h8-tWK"/> + <constraint firstItem="pEo-sN-P32" firstAttribute="leading" secondItem="ljw-sY-kbY" secondAttribute="trailing" constant="14" id="gq5-lU-NOx"/> + <constraint firstAttribute="trailingMargin" secondItem="Zmt-fY-s9P" secondAttribute="trailing" constant="40" id="rVw-f7-Vax"/> + <constraint firstItem="ljw-sY-kbY" firstAttribute="centerY" secondItem="pEo-sN-P32" secondAttribute="centerY" id="xpx-TF-OmA"/> + </constraints> + </tableViewCellContentView> + <connections> + <outlet property="accountNameLabel" destination="pEo-sN-P32" id="GlP-6d-tWw"/> + <outlet property="accountTypeLabel" destination="Zmt-fY-s9P" id="14Z-88-8oU"/> + <outlet property="activeSwitch" destination="ljw-sY-kbY" id="Mqy-YL-aKK"/> + </connections> + </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="addAccountTableCell" id="5Xh-6L-7id"> + <rect key="frame" x="0.0" y="72" width="375" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="5Xh-6L-7id" id="NQl-GU-fNn"> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="abD-8d-YV0" userLabel="Account Name Label"> + <rect key="frame" x="72" y="11" width="99" height="21"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="contactAdd" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="WqP-Hb-0XW"> + <rect key="frame" x="23" y="11" width="22" height="22"/> + <connections> + <action selector="addAccountClicked:" destination="RuW-kz-iBP" eventType="touchUpInside" id="hsq-5y-nrW"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstItem="WqP-Hb-0XW" firstAttribute="leading" secondItem="NQl-GU-fNn" secondAttribute="leadingMargin" constant="15" id="57r-PN-Cv9"/> + <constraint firstItem="abD-8d-YV0" firstAttribute="leading" secondItem="WqP-Hb-0XW" secondAttribute="trailing" constant="27" id="Y23-Qc-qx7"/> + <constraint firstItem="abD-8d-YV0" firstAttribute="top" secondItem="WqP-Hb-0XW" secondAttribute="top" id="Z57-DQ-a7x"/> + <constraint firstItem="WqP-Hb-0XW" firstAttribute="centerY" secondItem="NQl-GU-fNn" secondAttribute="centerY" id="r6D-Ma-YOX"/> + </constraints> + </tableViewCellContentView> + </tableViewCell> + </prototypes> + <connections> + <outlet property="dataSource" destination="RuW-kz-iBP" id="GfQ-Rx-J3H"/> + <outlet property="delegate" destination="RuW-kz-iBP" id="tH0-Pm-aAd"/> + </connections> + </tableView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PERSON PLACEHOLDER" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="w8x-Sv-T50"> + <rect key="frame" x="16" y="40" width="180" height="64"/> + <constraints> + <constraint firstAttribute="height" constant="64" id="j5c-ee-ima"/> + <constraint firstAttribute="width" constant="180" id="kVb-tA-4rc"/> + </constraints> + <fontDescription key="fontDescription" type="system" pointSize="22"/> + <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="trailing" secondItem="XF9-LL-io7" secondAttribute="trailing" id="718-Oh-eiW"/> + <constraint firstItem="XF9-LL-io7" firstAttribute="top" secondItem="pZo-fN-8Fj" secondAttribute="bottom" constant="42" id="78P-ZG-RJW"/> + <constraint firstItem="XF9-LL-io7" firstAttribute="leading" secondItem="nkh-py-Uar" secondAttribute="leading" id="XgK-Iq-WPC"/> + <constraint firstItem="XF9-LL-io7" firstAttribute="centerX" secondItem="pZo-fN-8Fj" secondAttribute="centerX" id="aqn-W8-WhD"/> + <constraint firstItem="tam-QD-Xpf" firstAttribute="top" secondItem="XF9-LL-io7" secondAttribute="bottom" id="fVp-mz-u7B"/> + <constraint firstItem="w8x-Sv-T50" firstAttribute="leading" secondItem="nkh-py-Uar" secondAttribute="leadingMargin" id="gCm-aq-fb9"/> + <constraint firstItem="w8x-Sv-T50" firstAttribute="top" secondItem="SYf-gf-IKh" secondAttribute="bottom" constant="20" id="wHN-au-sTr"/> + </constraints> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <tabBarItem key="tabBarItem" title="Me" id="AKc-Hb-EHr"/> + <navigationItem key="navigationItem" id="vC8-Ti-TTd"> + <barButtonItem key="rightBarButtonItem" systemItem="edit" id="NqP-iv-qav"/> + </navigationItem> + <connections> + <outlet property="accountTableView" destination="XF9-LL-io7" id="y6M-sn-b5R"/> + <outlet property="nameLabel" destination="w8x-Sv-T50" id="OAT-b7-q3U"/> + <outlet property="qrImageView" destination="pZo-fN-8Fj" id="tjL-5d-Kvk"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="MXJ-Bb-hGu" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-94" y="-438"/> + </scene> + </scenes> +</document> diff --git a/Ring/Ring/Settings/MeViewController.swift b/Ring/Ring/Features/Me/Me/MeViewController.swift similarity index 68% rename from Ring/Ring/Settings/MeViewController.swift rename to Ring/Ring/Features/Me/Me/MeViewController.swift index ae4c4df2d97755871578a130fac2ea085747fce1..05de47f1a51faf2c3f86b3e50838dbc0e034bdba 100644 --- a/Ring/Ring/Settings/MeViewController.swift +++ b/Ring/Ring/Features/Me/Me/MeViewController.swift @@ -19,33 +19,25 @@ */ import UIKit +import Reusable -class MeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { +class MeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, StoryboardBased, ViewModelBased { - // MARK: - Properties - let accountService = AppDelegate.accountService + // MARK: - outlets @IBOutlet weak var accountTableView: UITableView! @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var qrImageView: UIImageView! - // MARK: - UIViewController + // MARK: - members + var viewModel: MeViewModel! + + // MARK: - functions override func viewDidLoad() { super.viewDidLoad() self.title = L10n.Global.meTabBarTitle self.navigationItem.title = L10n.Global.meTabBarTitle - if !accountService.accounts.isEmpty { - // let acc = accountService.accounts[0] - // nameLabel.text = acc.displayName - // if let username = acc.username { - // createQRFromString(username); - // } - } - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() } // MARK: - QRCode @@ -71,19 +63,17 @@ class MeViewController: UIViewController, UITableViewDelegate, UITableViewDataSo } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return accountService.accounts.count + 1 + return self.viewModel.accountNumber + 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.row < accountService.accounts.count { + if indexPath.row < self.viewModel.accountNumber { let cell = tableView.dequeueReusableCell(for: indexPath, cellType: AccountTableViewCell.self) - let account = accountService.accounts[indexPath.row] + let account = self.viewModel.account(at: indexPath.row) cell.account = account - // cell.accountNameLabel.text = account.alias - // cell.activeSwitch.setOn(account.isEnabled, animated: false) - // cell.accountTypeLabel.text = account.accountType.rawValue + return cell } else { @@ -94,13 +84,13 @@ class MeViewController: UIViewController, UITableViewDelegate, UITableViewDataSo } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.row == accountService.accounts.count { + if indexPath.row == self.viewModel.accountNumber { accountTableView.reloadData() } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - if indexPath.row == accountService.accounts.count { + if indexPath.row == self.viewModel.accountNumber { return false } return true @@ -108,24 +98,15 @@ class MeViewController: UIViewController, UITableViewDelegate, UITableViewDataSo func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == UITableViewCellEditingStyle.delete { - accountService.removeAccount(indexPath.row) + self.viewModel.deleteAccount(at: indexPath.row) accountTableView.reloadData() } } // MARK: - Actions @IBAction func addAccountClicked(_ sender: AnyObject) { - let index = IndexPath(row: accountService.accounts.count, section: 0) + let index = IndexPath(row: self.viewModel.accountNumber, section: 0) accountTableView.selectRow(at: index, animated: false, scrollPosition: UITableViewScrollPosition.none) tableView(accountTableView, didSelectRowAt: index) } - - // MARK: - Navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "accountDetails", - let cell = sender as? AccountTableViewCell, - let vc = segue.destination as? AccountDetailsViewController { - vc.account = cell.account - } - } } diff --git a/Ring/Ring/Walkthrough/Cell/TextFieldCell.swift b/Ring/Ring/Features/Me/Me/MeViewModel.swift similarity index 51% rename from Ring/Ring/Walkthrough/Cell/TextFieldCell.swift rename to Ring/Ring/Features/Me/Me/MeViewModel.swift index 47f31cdcaeeef31eff551923d5d9f8b33ffdf79f..fa37bcbdebcb29c6a1aa19bc23e3f8870408cd20 100644 --- a/Ring/Ring/Walkthrough/Cell/TextFieldCell.swift +++ b/Ring/Ring/Features/Me/Me/MeViewModel.swift @@ -1,7 +1,7 @@ /* * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,23 +18,33 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import UIKit -import Reusable +import Foundation +import RxSwift -class TextFieldCell: UITableViewCell, NibReusable { +class MeViewModel: Stateable, ViewModel { - @IBOutlet weak var textField: UITextField! - @IBOutlet weak var errorMessageLabel: UILabel! + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code + var accountNumber: Int { + return self.accountService.accounts.count } - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) + let accountService: AccountsService - // Configure the view for the selected state + required init(with injectionBag: InjectionBag) { + self.accountService = injectionBag.accountService + } + + func account(at row: Int) -> AccountModel { + return self.accountService.accounts[row] + } + + func deleteAccount(at row: Int) { + self.accountService.removeAccount(row) } } diff --git a/Ring/Ring/Features/Me/MeCoordinator.swift b/Ring/Ring/Features/Me/MeCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..958d88ef1aae37acc09a91952590eff38f5b06d3 --- /dev/null +++ b/Ring/Ring/Features/Me/MeCoordinator.swift @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +/// Represents Me navigation state +/// +/// - meDetail: user want its account detail +public enum MeState: State { + case meDetail +} + +/// This Coordinator drives the me/settings navigation +class MeCoordinator: Coordinator, StateableResponsive { + + var rootViewController: UIViewController { + return self.navigationViewController + } + + var childCoordinators = [Coordinator]() + + private let navigationViewController = UINavigationController() + private let injectionBag: InjectionBag + let disposeBag = DisposeBag() + + let stateSubject = PublishSubject<State>() + + required init (with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + + self.stateSubject.subscribe(onNext: { [unowned self] (state) in + guard let state = state as? MeState else { return } + switch state { + case .meDetail: + self.showMeDetail() + break + } + }).disposed(by: self.disposeBag) + + } + + func start () { + let meViewController = MeViewController.instantiate(with: self.injectionBag) + self.present(viewController: meViewController, withStyle: .show, withAnimation: true, withStateable: meViewController.viewModel) + } + + private func showMeDetail () { + let meDetailViewController = MeDetailViewController.instantiate(with: self.injectionBag) + self.present(viewController: meDetailViewController, withStyle: .show, withAnimation: true) + } +} diff --git a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.storyboard b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..ef9b6c14988c1a3549252423e412f7dcb50b3573 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.storyboard @@ -0,0 +1,235 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="jUh-Lp-ulu"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Create Account View Controller--> + <scene sceneID="8fM-B7-Q9N"> + <objects> + <viewController id="jUh-Lp-ulu" customClass="CreateAccountViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="Wq5-qI-WER"/> + <viewControllerLayoutGuide type="bottom" id="fgQ-1h-rMi"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="7Ub-Rk-fjK"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="background_ring" translatesAutoresizingMaskIntoConstraints="NO" id="xYA-E3-7tv"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + </imageView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WZ7-L9-OuJ" userLabel="Gradient View" customClass="DesignableView" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <color key="backgroundColor" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="displayP3"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="color" keyPath="gradientEndColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="gradientStartColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.0" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </view> + <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UZB-Gb-Dp2"> + <rect key="frame" x="16" y="28" width="343" height="631"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="g3i-8d-O4a" userLabel="Container View" customClass="DesignableView" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="-16" y="-18" width="375" height="667"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Create your account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cAM-lf-Iex"> + <rect key="frame" x="79" y="100" width="217" height="29"/> + <fontDescription key="fontDescription" type="system" pointSize="24"/> + <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hzc-aD-41e"> + <rect key="frame" x="286" y="164" width="51" height="31"/> + </switch> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4YB-Ka-7tL"> + <rect key="frame" x="40" y="210" width="295" height="120"/> + <subviews> + <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="username" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="E18-Hh-7He" customClass="DesignableTextField" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="8" width="295" height="40"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="BAO-oa-Ecp"/> + </constraints> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="number" keyPath="borderWidth"> + <real key="value" value="0.0"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="borderColor"> + <color key="value" cocoaTouchSystemColor="darkTextColor"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius"> + <real key="value" value="0.0"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </textField> + <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="this username is not available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qDW-b2-4NQ"> + <rect key="frame" x="0.0" y="56" width="179" height="16"/> + <fontDescription key="fontDescription" type="system" pointSize="13"/> + <color key="textColor" red="0.83137254900000002" green="0.054901960780000002" blue="0.070588235289999995" alpha="1" colorSpace="calibratedRGB"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="trailing" secondItem="E18-Hh-7He" secondAttribute="trailing" id="0Nq-c5-EJ3"/> + <constraint firstItem="E18-Hh-7He" firstAttribute="top" secondItem="4YB-Ka-7tL" secondAttribute="top" constant="8" id="EkX-GA-dSB"/> + <constraint firstItem="qDW-b2-4NQ" firstAttribute="top" secondItem="E18-Hh-7He" secondAttribute="bottom" constant="8" symbolic="YES" id="S0t-Pw-oUV"/> + <constraint firstItem="E18-Hh-7He" firstAttribute="leading" secondItem="4YB-Ka-7tL" secondAttribute="leading" id="Y47-Gp-2mi"/> + <constraint firstAttribute="height" constant="120" id="atW-lO-zQN"/> + <constraint firstItem="qDW-b2-4NQ" firstAttribute="leading" secondItem="E18-Hh-7He" secondAttribute="leading" id="jnN-7G-KOd"/> + </constraints> + </view> + <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="password" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="jhr-UL-eCl" customClass="DesignableTextField" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="40" y="338" width="295" height="40"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="eGy-0v-Pm0"/> + </constraints> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" secureTextEntry="YES"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + <userDefinedRuntimeAttribute type="number" keyPath="borderWidth"> + <real key="value" value="0.0"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="borderColor"> + <color key="value" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </textField> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Register a username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qeI-Uf-g3M"> + <rect key="frame" x="40" y="169" width="157" height="21"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="confirm password" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ODL-4B-fCm" customClass="DesignableTextField" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="40" y="386" width="295" height="40"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="iUA-8a-sP7"/> + </constraints> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" secureTextEntry="YES"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + <userDefinedRuntimeAttribute type="number" keyPath="borderWidth"> + <real key="value" value="0.0"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="borderColor"> + <color key="value" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </textField> + <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="passwords do not match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IWj-x1-hN9"> + <rect key="frame" x="40" y="434" width="149" height="16"/> + <fontDescription key="fontDescription" type="system" pointSize="13"/> + <color key="textColor" red="0.83137254900000002" green="0.054901960780000002" blue="0.070588235289999995" alpha="1" colorSpace="calibratedRGB"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="05l-0O-Ql0" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="87" y="490" width="200" height="40"/> + <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="501-bI-E5J"/> + <constraint firstAttribute="width" constant="200" id="ylQ-Ok-Sqa"/> + </constraints> + <color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <state key="normal" title="Create account"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + </subviews> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="trailing" secondItem="hzc-aD-41e" secondAttribute="trailing" constant="40" id="1wO-3T-tfO"/> + <constraint firstItem="cAM-lf-Iex" firstAttribute="centerX" secondItem="g3i-8d-O4a" secondAttribute="centerX" id="5Sz-Rn-XXy"/> + <constraint firstItem="05l-0O-Ql0" firstAttribute="top" secondItem="IWj-x1-hN9" secondAttribute="bottom" constant="40" id="BAN-gU-ylu"/> + <constraint firstItem="jhr-UL-eCl" firstAttribute="trailing" secondItem="E18-Hh-7He" secondAttribute="trailing" id="BU5-3N-9mA"/> + <constraint firstItem="4YB-Ka-7tL" firstAttribute="trailing" secondItem="hzc-aD-41e" secondAttribute="trailing" id="CWP-6L-YHH"/> + <constraint firstItem="ODL-4B-fCm" firstAttribute="trailing" secondItem="jhr-UL-eCl" secondAttribute="trailing" id="EjA-RC-FzN"/> + <constraint firstItem="ODL-4B-fCm" firstAttribute="top" secondItem="jhr-UL-eCl" secondAttribute="bottom" constant="8" symbolic="YES" id="Fr5-Jq-SZ3"/> + <constraint firstItem="IWj-x1-hN9" firstAttribute="top" secondItem="ODL-4B-fCm" secondAttribute="bottom" constant="8" id="IGh-ug-cAv"/> + <constraint firstItem="qeI-Uf-g3M" firstAttribute="leading" secondItem="g3i-8d-O4a" secondAttribute="leading" constant="40" id="JQv-wS-uUg"/> + <constraint firstItem="qeI-Uf-g3M" firstAttribute="top" secondItem="cAM-lf-Iex" secondAttribute="bottom" constant="40" id="QUu-qc-tj4"/> + <constraint firstItem="IWj-x1-hN9" firstAttribute="leading" secondItem="ODL-4B-fCm" secondAttribute="leading" id="SYS-B9-z5L"/> + <constraint firstItem="ODL-4B-fCm" firstAttribute="leading" secondItem="jhr-UL-eCl" secondAttribute="leading" id="Yal-mT-NwY"/> + <constraint firstItem="4YB-Ka-7tL" firstAttribute="top" secondItem="qeI-Uf-g3M" secondAttribute="bottom" constant="20" id="YwD-hf-TpI"/> + <constraint firstItem="jhr-UL-eCl" firstAttribute="leading" secondItem="E18-Hh-7He" secondAttribute="leading" id="ZG8-5Q-pwI"/> + <constraint firstItem="05l-0O-Ql0" firstAttribute="centerX" secondItem="g3i-8d-O4a" secondAttribute="centerX" id="fDC-6U-Uy1"/> + <constraint firstItem="jhr-UL-eCl" firstAttribute="top" secondItem="4YB-Ka-7tL" secondAttribute="bottom" constant="8" symbolic="YES" id="iUo-Vt-uih"/> + <constraint firstAttribute="bottom" secondItem="05l-0O-Ql0" secondAttribute="bottom" constant="137" id="pRs-R7-cGv"/> + <constraint firstItem="hzc-aD-41e" firstAttribute="centerY" secondItem="qeI-Uf-g3M" secondAttribute="centerY" id="quS-Vm-CiE"/> + <constraint firstItem="cAM-lf-Iex" firstAttribute="top" secondItem="g3i-8d-O4a" secondAttribute="top" constant="100" id="sCC-hK-7zg"/> + <constraint firstItem="4YB-Ka-7tL" firstAttribute="leading" secondItem="qeI-Uf-g3M" secondAttribute="leading" id="yzC-hd-Rhc"/> + </constraints> + </view> + </subviews> + <constraints> + <constraint firstItem="g3i-8d-O4a" firstAttribute="leading" secondItem="UZB-Gb-Dp2" secondAttribute="leading" constant="-16" id="21l-gk-aAy"/> + <constraint firstAttribute="trailing" secondItem="g3i-8d-O4a" secondAttribute="trailing" constant="-16" id="4vn-no-Pzq"/> + <constraint firstItem="g3i-8d-O4a" firstAttribute="top" secondItem="UZB-Gb-Dp2" secondAttribute="top" constant="-18" id="AXm-Ng-PDJ"/> + <constraint firstAttribute="bottom" secondItem="g3i-8d-O4a" secondAttribute="bottom" constant="-18" id="zJ5-Tt-cVQ"/> + </constraints> + </scrollView> + </subviews> + <color key="backgroundColor" red="1" green="0.093435211690000006" blue="0.097165417209999994" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> + <constraints> + <constraint firstItem="xYA-E3-7tv" firstAttribute="leading" secondItem="7Ub-Rk-fjK" secondAttribute="leading" id="1au-pT-kOj"/> + <constraint firstItem="WZ7-L9-OuJ" firstAttribute="top" secondItem="7Ub-Rk-fjK" secondAttribute="top" id="44e-Ab-mbh"/> + <constraint firstItem="g3i-8d-O4a" firstAttribute="height" secondItem="7Ub-Rk-fjK" secondAttribute="height" priority="250" id="7wt-Vd-eXx"/> + <constraint firstItem="xYA-E3-7tv" firstAttribute="top" secondItem="7Ub-Rk-fjK" secondAttribute="top" id="MWk-0f-CtJ"/> + <constraint firstItem="fgQ-1h-rMi" firstAttribute="top" secondItem="UZB-Gb-Dp2" secondAttribute="bottom" constant="8" symbolic="YES" id="Nff-ZG-Yuu"/> + <constraint firstItem="WZ7-L9-OuJ" firstAttribute="leading" secondItem="7Ub-Rk-fjK" secondAttribute="leading" id="PY4-51-dTX"/> + <constraint firstAttribute="trailingMargin" secondItem="UZB-Gb-Dp2" secondAttribute="trailing" id="ZeU-39-hYg"/> + <constraint firstItem="fgQ-1h-rMi" firstAttribute="top" secondItem="WZ7-L9-OuJ" secondAttribute="bottom" id="Zgp-E4-04L"/> + <constraint firstAttribute="trailing" secondItem="xYA-E3-7tv" secondAttribute="trailing" id="haz-3o-WbQ"/> + <constraint firstAttribute="trailing" secondItem="WZ7-L9-OuJ" secondAttribute="trailing" id="ko3-9b-RwG"/> + <constraint firstItem="UZB-Gb-Dp2" firstAttribute="top" secondItem="Wq5-qI-WER" secondAttribute="bottom" constant="8" symbolic="YES" id="l58-kW-bAF"/> + <constraint firstAttribute="leadingMargin" secondItem="UZB-Gb-Dp2" secondAttribute="leading" id="n4K-YN-Ab0"/> + <constraint firstItem="fgQ-1h-rMi" firstAttribute="top" secondItem="xYA-E3-7tv" secondAttribute="bottom" id="nz7-EU-vy2"/> + <constraint firstItem="g3i-8d-O4a" firstAttribute="width" secondItem="7Ub-Rk-fjK" secondAttribute="width" id="wcZ-tz-DCM"/> + </constraints> + </view> + <connections> + <outlet property="confirmPasswordTextField" destination="ODL-4B-fCm" id="CNa-S1-DmO"/> + <outlet property="createAccountButton" destination="05l-0O-Ql0" id="UQq-NA-axm"/> + <outlet property="createAccountTitle" destination="cAM-lf-Iex" id="6df-0w-qzU"/> + <outlet property="passwordErrorLabel" destination="IWj-x1-hN9" id="VhJ-5w-sS4"/> + <outlet property="passwordTextField" destination="jhr-UL-eCl" id="rmW-PB-Fdo"/> + <outlet property="registerUsernameErrorLabel" destination="qDW-b2-4NQ" id="5uc-Js-bZ1"/> + <outlet property="registerUsernameHeightConstraint" destination="atW-lO-zQN" id="SaG-wi-hZG"/> + <outlet property="registerUsernameLabel" destination="qeI-Uf-g3M" id="vk3-PS-YeD"/> + <outlet property="registerUsernameView" destination="4YB-Ka-7tL" id="ge5-dV-TMa"/> + <outlet property="scrollView" destination="UZB-Gb-Dp2" id="oGn-Z2-qU2"/> + <outlet property="usernameSwitch" destination="hzc-aD-41e" id="c8Y-bI-KEV"/> + <outlet property="usernameTextField" destination="E18-Hh-7He" id="t2T-vd-2a3"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="TXV-Gn-qlq" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1944.8" y="563.56821589205401"/> + </scene> + </scenes> + <resources> + <image name="background_ring" width="750" height="1334"/> + </resources> +</document> diff --git a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.swift b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..e008e16c1a77ca3edee1059dcf485f00f6ebb4e4 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewController.swift @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import UIKit +import Reusable +import RxSwift +import PKHUD + +class CreateAccountViewController: UIViewController, StoryboardBased, ViewModelBased { + + // MARK: outlets + @IBOutlet weak var createAccountButton: DesignableButton! + @IBOutlet weak var createAccountTitle: UILabel! + @IBOutlet weak var registerUsernameHeightConstraint: NSLayoutConstraint! { + didSet { + self.registerUsernameHeightConstraintConstant = registerUsernameHeightConstraint.constant + } + } + @IBOutlet weak var usernameSwitch: UISwitch! + @IBOutlet weak var registerUsernameView: UIView! + @IBOutlet weak var registerUsernameLabel: UILabel! + @IBOutlet weak var registerUsernameErrorLabel: UILabel! + @IBOutlet weak var passwordTextField: DesignableTextField! + @IBOutlet weak var confirmPasswordTextField: DesignableTextField! + @IBOutlet weak var passwordErrorLabel: UILabel! + @IBOutlet weak var usernameTextField: DesignableTextField! + @IBOutlet weak var scrollView: UIScrollView! + + // MARK: members + private let disposeBag = DisposeBag() + var viewModel: CreateAccountViewModel! + var registerUsernameHeightConstraintConstant: CGFloat = 0.0 + + // MARK: functions + override func viewDidLoad() { + super.viewDidLoad() + + // L10n + self.applyL10n() + + // Bind ViewModel to View + self.bindViewModelToView() + + // Bind Voew to ViewModel + self.bindViewToViewModel() + + // handle keyboard + self.adaptToKeyboardState(for: self.scrollView, with: self.disposeBag) + } + + private func applyL10n() { + self.createAccountTitle.text = self.viewModel.createAccountTitle + self.createAccountButton.setTitle(self.viewModel.createAccountButton, for: .normal) + self.usernameTextField.placeholder = self.viewModel.usernameTitle + self.passwordTextField.placeholder = self.viewModel.passwordTitle + self.confirmPasswordTextField.placeholder = self.viewModel.confirmPasswordTitle + } + + private func bindViewModelToView() { + // handle username registration visibility + self.viewModel.registerUsername.asObservable().subscribe(onNext: { [unowned self] (isOn) in + UIView.animate(withDuration: 0.3, animations: { + if isOn { + self.registerUsernameHeightConstraint.constant = self.registerUsernameHeightConstraintConstant + self.registerUsernameView.alpha = 1.0 + } else { + self.registerUsernameHeightConstraint.constant = 0 + self.registerUsernameView.alpha = 0.0 + } + + self.view.layoutIfNeeded() + }) + }).disposed(by: self.disposeBag) + + // handle Create Account Button state + self.viewModel.canAskForAccountCreation.bind(to: self.createAccountButton.rx.isEnabled).disposed(by: self.disposeBag) + + // handle password error + self.viewModel.passwordValidationState.map { $0.isValidated } + .skipUntil(self.passwordTextField.rx.controlEvent(UIControlEvents.editingDidEnd)) + .bind(to: self.passwordErrorLabel.rx.isHidden).disposed(by: self.disposeBag) + self.viewModel.passwordValidationState.map { $0.message } + .skipUntil(self.passwordTextField.rx.controlEvent(UIControlEvents.editingDidEnd)) + .bind(to: self.passwordErrorLabel.rx.text).disposed(by: self.disposeBag) + + // handle registration error + self.viewModel.usernameValidationState.asObservable().map { $0.isAvailable } + .skipUntil(self.usernameTextField.rx.controlEvent(UIControlEvents.editingDidBegin)) + .bind(to: self.registerUsernameErrorLabel.rx.isHidden).disposed(by: self.disposeBag) + self.viewModel.usernameValidationState.asObservable().map { $0.message } + .skipUntil(self.usernameTextField.rx.controlEvent(UIControlEvents.editingDidBegin)) + .bind(to: self.registerUsernameErrorLabel.rx.text).disposed(by: self.disposeBag) + + // handle creation state + self.viewModel.createState.subscribe(onNext: { [weak self] (state) in + switch state { + case .started: + self?.showAccountCreationInProgress() + break + case .success: + self?.hideAccountCreationHud() + break + default: + self?.hideAccountCreationHud() + break + } + }, onError: { [weak self] (error) in + self?.hideAccountCreationHud() + + if let error = error as? AccountCreationError { + self?.showAccountCreationError(error: error) + } + }).disposed(by: self.disposeBag) + } + + private func bindViewToViewModel() { + // Bind View Outlets to ViewModel + self.usernameSwitch.rx.isOn.bind(to: self.viewModel.registerUsername).disposed(by: self.disposeBag) + self.usernameTextField.rx.text.orEmpty.throttle(3, scheduler: MainScheduler.instance).distinctUntilChanged().bind(to: self.viewModel.username).disposed(by: self.disposeBag) + self.passwordTextField.rx.text.orEmpty.bind(to: self.viewModel.password).disposed(by: self.disposeBag) + self.confirmPasswordTextField.rx.text.orEmpty.bind(to: self.viewModel.confirmPassword).disposed(by: self.disposeBag) + + // Bind View Actions to ViewModel + self.createAccountButton.rx.tap.subscribe(onNext: { [unowned self] in + self.viewModel.createAccount() + }).disposed(by: self.disposeBag) + } + + private func showAccountCreationInProgress() { + HUD.show(.labeledProgress(title: L10n.Createaccount.waitCreateAccountTitle, subtitle: nil)) + } + + private func showAccountCreationSuccess() { + HUD.flash(.labeledSuccess(title: L10n.Alerts.accountAddedTitle, subtitle: nil), delay: Durations.alertFlashDuration.value) + } + + private func hideAccountCreationHud() { + HUD.hide() + } + + private func showAccountCreationError(error: AccountCreationError) { + let alert = UIAlertController.init(title: error.title, + message: error.message, + preferredStyle: .alert) + alert.addAction(UIAlertAction.init(title: L10n.Global.ok, style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } +} diff --git a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..7adedbcfa07b85eaea07deb90714bde0ca2754c5 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +enum PasswordValidationState { + case validated + case error (message: String) + + var isValidated: Bool { + switch self { + case .validated: + return true + default: + return false + } + } + + var message: String { + switch self { + case .validated: + return "" + case .error(let message): + return message + } + } +} + +enum UsernameValidationState { + case unknown + case available + case lookingForAvailibility(message: String) + case invalid(message: String) + case unavailable(message: String) + + var isAvailable: Bool { + switch self { + case .available, .unknown: + return true + default: + return false + } + } + + var message: String { + switch self { + case .available, .unknown: + return "" + case .lookingForAvailibility(let message): + return message + case .invalid(let message): + return message + case .unavailable(let message): + return message + } + } +} + +enum AccountCreationState { + case unknown + case started + case success + case error(error: AccountCreationError) + + var isInProgress: Bool { + switch self { + case .started: + return true + default: + return false + } + } + + var message: String { + switch self { + case .error(let error): + return error.localizedDescription + default: + return "" + } + } +} + +enum AccountCreationError: Error { + case generic + case network + case unknown +} + +extension AccountCreationError: LocalizedError { + + var title: String { + switch self { + case .generic: + return L10n.Alerts.accountCannotBeFoundTitle + case .network: + return L10n.Alerts.accountNoNetworkTitle + default: + return L10n.Alerts.accountDefaultErrorTitle + } + } + + var message: String { + switch self { + case .generic: + return L10n.Alerts.accountDefaultErrorMessage + case .network: + return L10n.Alerts.accountNoNetworkMessage + default: + return L10n.Alerts.accountDefaultErrorMessage + } + } +} + +// swiftlint:disable opening_brace +// swiftlint:disable closure_parameter_position +class CreateAccountViewModel: Stateable, ViewModel { + + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() + + private let disposeBag = DisposeBag() + + // MARK: L10n + let createAccountTitle = L10n.Createaccount.createAccountFormTitle + let createAccountButton = L10n.Welcome.createAccount + let usernameTitle = L10n.Createaccount.enterNewUsernamePlaceholder + let passwordTitle = L10n.Createaccount.newPasswordPlaceholder + let confirmPasswordTitle = L10n.Createaccount.repeatPasswordPlaceholder + + // MARK: - Low level services + private let accountService: AccountsService + private let nameService: NameService + + // MARK: - Rx Variables for UI binding + private let accountCreationState = Variable<AccountCreationState>(.unknown) + lazy var createState: Observable<AccountCreationState> = { + return self.accountCreationState.asObservable() + }() + let username = Variable<String>("") + let password = Variable<String>("") + let confirmPassword = Variable<String>("") + let registerUsername = Variable<Bool>(true) + lazy var passwordValidationState: Observable<PasswordValidationState> = { + return Observable.combineLatest(self.password.asObservable(), self.confirmPassword.asObservable()) + { (password: String, confirmPassword: String) -> PasswordValidationState in + if password.characters.count < 6 { + return .error(message: L10n.Createaccount.passwordCharactersNumberError) + } + + if password != confirmPassword { + return .error(message: L10n.Createaccount.passwordNotMatchingError) + } + + return .validated + } + }() + lazy var usernameValidationState = Variable<UsernameValidationState>(.unknown) + lazy var canAskForAccountCreation: Observable<Bool> = { + return Observable.combineLatest(self.passwordValidationState.asObservable(), + self.usernameValidationState.asObservable(), + self.registerUsername.asObservable(), + self.createState, + resultSelector: + { ( passwordValidationState: PasswordValidationState, + usernameValidationState: UsernameValidationState, + registerUsername: Bool, + creationState: AccountCreationState) -> Bool in + + var canAsk = true + + if registerUsername { + canAsk = canAsk && usernameValidationState.isAvailable + } + + canAsk = canAsk && passwordValidationState.isValidated + + canAsk = canAsk && !creationState.isInProgress + + return canAsk + }) + }() + + required init (with injectionBag: InjectionBag) { + self.accountService = injectionBag.accountService + self.nameService = injectionBag.nameService + + //Loookup name request observer + self.username.asObservable().subscribe(onNext: { [unowned self] username in + self.nameService.lookupName(withAccount: "", nameserver: "", name: username) + }).disposed(by: disposeBag) + + self.nameService.usernameValidationStatus.asObservable().subscribe(onNext: { [weak self] (status) in + switch status { + case .lookingUp: + self?.usernameValidationState.value = .lookingForAvailibility(message: L10n.Createaccount.lookingForUsernameAvailability) + break + case .invalid: + self?.usernameValidationState.value = .invalid(message: L10n.Createaccount.invalidUsername) + break + case .alreadyTaken: + self?.usernameValidationState.value = .unavailable(message: L10n.Createaccount.usernameAlreadyTaken) + break + default: + self?.usernameValidationState.value = .available + } + }).disposed(by: self.disposeBag) + + //Name registration observer + self.accountService + .sharedResponseStream + .filter({ [unowned self] (event) in + return event.eventType == ServiceEventType.registrationStateChanged && + event.getEventInput(ServiceEventInput.registrationState) == Unregistered && + self.registerUsername.value + }) + .subscribe(onNext: { [unowned self] _ in + + //Launch the process of name registration + if let currentAccountId = self.accountService.currentAccount?.id { + self.nameService.registerName(withAccount: currentAccountId, + password: self.password.value, + name: self.username.value) + } + }) + .disposed(by: disposeBag) + + //Account creation state observer + self.accountService + .sharedResponseStream + .subscribe(onNext: { [unowned self] event in + if event.getEventInput(ServiceEventInput.registrationState) == Unregistered { + self.accountCreationState.value = .success + Observable<Int>.timer(Durations.alertFlashDuration.value, period: nil, scheduler: MainScheduler.instance).subscribe(onNext: { [unowned self] (_) in + self.stateSubject.onNext(WalkthroughState.accountCreated) + }).disposed(by: self.disposeBag) + } else if event.getEventInput(ServiceEventInput.registrationState) == ErrorGeneric { + self.accountCreationState.value = .error(error: AccountCreationError.generic) + } else if event.getEventInput(ServiceEventInput.registrationState) == ErrorNetwork { + self.accountCreationState.value = .error(error: AccountCreationError.network) + } + }, onError: { [unowned self] _ in + self.accountCreationState.value = .error(error: AccountCreationError.unknown) + }).disposed(by: disposeBag) + + } + + func createAccount() { + self.accountCreationState.value = .started + self.accountService.addRingAccount(withUsername: self.username.value, + password: self.password.value) + } +} diff --git a/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.storyboard b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..c931a93387ac39c0f8470b9077f06cea37859aa1 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.storyboard @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="1yn-Mj-8Ek"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Create Profile View Controller--> + <scene sceneID="tJD-hy-eho"> + <objects> + <viewController id="1yn-Mj-8Ek" customClass="CreateProfileViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="jiD-fm-HFk"/> + <viewControllerLayoutGuide type="bottom" id="GVt-PH-FqG"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="N1T-Xh-FH1"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="background_ring" translatesAutoresizingMaskIntoConstraints="NO" id="Gv4-18-FVt"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + </imageView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kur-G7-4Nq" userLabel="Gradient View" customClass="DesignableView" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <color key="backgroundColor" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="displayP3"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="color" keyPath="gradientEndColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="gradientStartColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.0" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </view> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5Po-6e-k14" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="87.5" y="313.5" width="200" height="40"/> + <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="width" constant="200" id="m9R-vQ-tPe"/> + <constraint firstAttribute="height" constant="40" id="uVC-dY-9Ym"/> + </constraints> + <state key="normal" title="Skip"> + <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="Gv4-18-FVt" secondAttribute="trailing" id="1Ev-WL-COb"/> + <constraint firstAttribute="trailing" secondItem="kur-G7-4Nq" secondAttribute="trailing" id="1U8-xt-ygk"/> + <constraint firstItem="5Po-6e-k14" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="4eR-3k-6Z2"/> + <constraint firstItem="Gv4-18-FVt" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="5PT-u8-86G"/> + <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="Gv4-18-FVt" secondAttribute="bottom" id="DHv-Q6-GhU"/> + <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="kur-G7-4Nq" secondAttribute="bottom" id="Jvj-VY-Nb7"/> + <constraint firstItem="Gv4-18-FVt" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="LiW-7Y-wcc"/> + <constraint firstItem="kur-G7-4Nq" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="hSt-o1-S41"/> + <constraint firstItem="5Po-6e-k14" firstAttribute="centerY" secondItem="N1T-Xh-FH1" secondAttribute="centerY" id="lQW-P9-Vmv"/> + <constraint firstItem="kur-G7-4Nq" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="r5d-rQ-Kg3"/> + </constraints> + </view> + <connections> + <outlet property="skipButton" destination="5Po-6e-k14" id="DUG-Am-Mwn"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="6ma-i4-SuK" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1104.8" y="366.56671664167919"/> + </scene> + </scenes> + <resources> + <image name="background_ring" width="750" height="1334"/> + </resources> +</document> diff --git a/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.swift b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..bab3f5a8170048c0175183625d3da905af2433f5 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewController.swift @@ -0,0 +1,35 @@ +// +// CreateProfileViewController.swift +// Ring +// +// Created by Thibault Wittemberg on 2017-07-18. +// Copyright © 2017 Savoir-faire Linux. All rights reserved. +// + +import UIKit +import Reusable +import RxSwift + +class CreateProfileViewController: UIViewController, StoryboardBased, ViewModelBased { + + // MARK: outlets + @IBOutlet weak var skipButton: DesignableButton! + + // MARK: members + private let disposeBag = DisposeBag() + var viewModel: CreateProfileViewModel! + + // MARK: functions + override func viewDidLoad() { + super.viewDidLoad() + + // Bind ViewModel to View + self.viewModel.skipButtonTitle.bind(to: self.skipButton.rx.title(for: .normal)).disposed(by: self.disposeBag) + + // Bind View Actions to ViewModel + self.skipButton.rx.tap.subscribe(onNext: { [unowned self] in + self.viewModel.proceedWithAccountCreationOrDeviceLink() + }).disposed(by: self.disposeBag) + } + +} diff --git a/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewModel.swift b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..e0ec8421c8333710cf2b4eb0a1bc6cf73afa6822 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/CreateProfile/CreateProfileViewModel.swift @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +class CreateProfileViewModel: Stateable, ViewModel { + + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() + + // MARK: - Rx Singles for L10n + lazy var skipButtonTitle: Observable<String> = { + if self.walkthroughType == .createAccount { + return Observable<String>.of(L10n.Createprofile.createAccount) + } else { + return Observable<String>.of(L10n.Createprofile.linkDevice) + } + + }() + + var walkthroughType: WalkthroughType! + + required init (with injectionBag: InjectionBag) { + } + + func proceedWithAccountCreationOrDeviceLink() { + self.stateSubject.onNext(WalkthroughState.profileCreated(withType: self.walkthroughType)) + } + +} diff --git a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..92385380446b885d53abeb4e216f0397479669a2 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="1yn-Mj-8Ek"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Link Device View Controller--> + <scene sceneID="tJD-hy-eho"> + <objects> + <viewController id="1yn-Mj-8Ek" customClass="LinkDeviceViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="jiD-fm-HFk"/> + <viewControllerLayoutGuide type="bottom" id="GVt-PH-FqG"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="N1T-Xh-FH1"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="background_ring" translatesAutoresizingMaskIntoConstraints="NO" id="Gv4-18-FVt"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + </imageView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kur-G7-4Nq" userLabel="Gradient View" customClass="DesignableView" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <color key="backgroundColor" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="displayP3"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="color" keyPath="gradientEndColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="gradientStartColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.0" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </view> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5Po-6e-k14" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="87.5" y="313.5" width="200" height="40"/> + <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="width" constant="200" id="m9R-vQ-tPe"/> + <constraint firstAttribute="height" constant="40" id="uVC-dY-9Ym"/> + </constraints> + <state key="normal" title="Link Device"> + <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="Gv4-18-FVt" secondAttribute="trailing" id="1Ev-WL-COb"/> + <constraint firstAttribute="trailing" secondItem="kur-G7-4Nq" secondAttribute="trailing" id="1U8-xt-ygk"/> + <constraint firstItem="5Po-6e-k14" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="4eR-3k-6Z2"/> + <constraint firstItem="Gv4-18-FVt" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="5PT-u8-86G"/> + <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="Gv4-18-FVt" secondAttribute="bottom" id="DHv-Q6-GhU"/> + <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="kur-G7-4Nq" secondAttribute="bottom" id="Jvj-VY-Nb7"/> + <constraint firstItem="Gv4-18-FVt" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="LiW-7Y-wcc"/> + <constraint firstItem="kur-G7-4Nq" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="hSt-o1-S41"/> + <constraint firstItem="5Po-6e-k14" firstAttribute="centerY" secondItem="N1T-Xh-FH1" secondAttribute="centerY" id="lQW-P9-Vmv"/> + <constraint firstItem="kur-G7-4Nq" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="r5d-rQ-Kg3"/> + </constraints> + </view> + <connections> + <outlet property="linkButton" destination="5Po-6e-k14" id="OoP-zB-985"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="6ma-i4-SuK" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1104.8" y="366.56671664167919"/> + </scene> + </scenes> + <resources> + <image name="background_ring" width="750" height="1334"/> + </resources> +</document> diff --git a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..fcfddf3e1b53dce12ab9059f6514730290f7324b --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift @@ -0,0 +1,31 @@ +// +// CreateProfileViewController.swift +// Ring +// +// Created by Thibault Wittemberg on 2017-07-18. +// Copyright © 2017 Savoir-faire Linux. All rights reserved. +// + +import UIKit +import Reusable +import RxSwift + +class LinkDeviceViewController: UIViewController, StoryboardBased, ViewModelBased { + + // MARK: outlets + @IBOutlet weak var linkButton: DesignableButton! + + // MARK: members + private let disposeBag = DisposeBag() + var viewModel: LinkDeviceViewModel! + + // MARK: functions + override func viewDidLoad() { + super.viewDidLoad() + + self.linkButton.rx.tap.subscribe(onNext: { [unowned self] (_) in + self.viewModel.linkDevice() + }).disposed(by: self.disposeBag) + } + +} diff --git a/Ring/Ring/Walkthrough/Cell/TextCell.swift b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift similarity index 62% rename from Ring/Ring/Walkthrough/Cell/TextCell.swift rename to Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift index 829592bb588ab37d930cda107758e78c1a90ef20..71bba384baf915ed99642b81b891988b67c8ef1b 100644 --- a/Ring/Ring/Walkthrough/Cell/TextCell.swift +++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift @@ -1,7 +1,7 @@ /* * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,22 +18,22 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import UIKit -import Reusable +import Foundation +import RxSwift -class TextCell: UITableViewCell, NibReusable { +class LinkDeviceViewModel: Stateable, ViewModel { - @IBOutlet weak var label: UILabel! + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code + required init (with injectionBag: InjectionBag) { } - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state + func linkDevice () { + self.stateSubject.onNext(WalkthroughState.deviceLinked) } } diff --git a/Ring/Ring/Features/Walkthrough/WalkthroughCoordinator.swift b/Ring/Ring/Features/Walkthrough/WalkthroughCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..00383f49bbc2cf51ebea6e94375461b262cb29b7 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/WalkthroughCoordinator.swift @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +/// Represents the choice made by the user in the Walkthrough for the creation account type +/// +/// - createAccount: create an account from scratch (profile / username / password) +/// - linkDevice: link the device to an existing account (profile / pin / password) +public enum WalkthroughType { + case createAccount + case linkDevice +} + +/// Represents walkthrough navigation state +/// +/// - welcomeDone: user has made the WalkthroughType choice (first page) +/// - profileCreated: profile has been created +/// - accountCreated: account has finish creating +/// - deviceLinked: linking has finished +public enum WalkthroughState: State { + case welcomeDone(withType: WalkthroughType) + case profileCreated(withType: WalkthroughType) + case accountCreated + case deviceLinked +} + +/// This Coordinator drives the walkthrough navigation (welcome / profile / creation or link) +class WalkthroughCoordinator: Coordinator, StateableResponsive { + + var rootViewController: UIViewController { + return self.navigationViewController + } + + var childCoordinators = [Coordinator]() + + private let navigationViewController = UINavigationController() + private let injectionBag: InjectionBag + let disposeBag = DisposeBag() + + let stateSubject = PublishSubject<State>() + + required init (with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + + self.stateSubject.subscribe(onNext: { [unowned self] (state) in + guard let state = state as? WalkthroughState else { return } + switch state { + case .welcomeDone(let walkthroughType): + self.showCreateProfile(with: walkthroughType) + break + case .profileCreated(let walkthroughType): + self.showFinalStep(with: walkthroughType) + break + case .accountCreated, .deviceLinked: + self.rootViewController.dismiss(animated: true, completion: nil) + break + } + }).disposed(by: self.disposeBag) + + } + + func start () { + let welcomeViewController = WelcomeViewController.instantiate(with: self.injectionBag) + self.present(viewController: welcomeViewController, withStyle: .show, withAnimation: true, withStateable: welcomeViewController.viewModel) + } + + private func showCreateProfile (with walkthroughType: WalkthroughType) { + let createProfileViewController = CreateProfileViewController.instantiate(with: self.injectionBag) + createProfileViewController.viewModel.walkthroughType = walkthroughType + self.present(viewController: createProfileViewController, withStyle: .show, withAnimation: true, withStateable: createProfileViewController.viewModel) + } + + private func showFinalStep (with walkthroughType: WalkthroughType) { + if walkthroughType == .createAccount { + let createAccountViewController = CreateAccountViewController.instantiate(with: self.injectionBag) + self.present(viewController: createAccountViewController, withStyle: .show, withAnimation: true, withStateable: createAccountViewController.viewModel) + } else { + let linkDeviceViewController = LinkDeviceViewController.instantiate(with: self.injectionBag) + self.present(viewController: linkDeviceViewController, withStyle: .show, withAnimation: true, withStateable: linkDeviceViewController.viewModel) + } + } +} diff --git a/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.storyboard b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..1b3784e74efb8aceab1997bfe815dcd5baed4d67 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.storyboard @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ILs-xb-iKr"> + <device id="retina4_7" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> + <capability name="Aspect ratio constraints" minToolsVersion="5.1"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Welcome View Controller--> + <scene sceneID="4AN-bE-bMJ"> + <objects> + <viewController id="ILs-xb-iKr" customClass="WelcomeViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="ETz-GA-Th5"/> + <viewControllerLayoutGuide type="bottom" id="kDG-lW-7DA"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="Dg0-kS-rT7"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="background_ring" translatesAutoresizingMaskIntoConstraints="NO" id="11r-JK-mPT"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + </imageView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8TU-za-C7U" userLabel="Gradient View" customClass="DesignableView" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <color key="backgroundColor" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="displayP3"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="color" keyPath="gradientStartColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="0.0" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="color" keyPath="gradientEndColor"> + <color key="value" red="0.2274509804" green="0.75294117650000003" blue="0.82352941180000006" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </view> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" verticalHuggingPriority="251" image="logo-ring-beta2-blanc" translatesAutoresizingMaskIntoConstraints="NO" id="2Pc-uJ-SAI"> + <rect key="frame" x="97.5" y="233" width="180" height="66"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="width" secondItem="2Pc-uJ-SAI" secondAttribute="height" multiplier="30:11" id="Qre-jC-ga0"/> + </constraints> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Ring is a free and universal communication platform which preserves the users' privacy and freedoms" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="E4b-Zv-unB"> + <rect key="frame" x="62.5" y="356" width="250" height="47"/> + <fontDescription key="fontDescription" type="system" pointSize="13"/> + <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="750" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Welcome to Ring" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lbn-cd-UXk"> + <rect key="frame" x="97" y="319" width="180.5" height="29"/> + <fontDescription key="fontDescription" type="system" pointSize="24"/> + <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QLK-gs-fOJ" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="62.5" y="503" width="250" height="40"/> + <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="YW4-Sc-61P"/> + </constraints> + <state key="normal" title="Link this device to an account"> + <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bu0-90-MB5" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="62.5" y="443" width="250" height="40"/> + <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="FEf-kf-jCs"/> + <constraint firstAttribute="width" constant="250" id="epE-7u-SMQ"/> + </constraints> + <state key="normal" title="Create a Ring account"> + <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + </subviews> + <constraints> + <constraint firstItem="bu0-90-MB5" firstAttribute="top" secondItem="E4b-Zv-unB" secondAttribute="bottom" constant="40" id="0hJ-6T-D3J"/> + <constraint firstItem="Lbn-cd-UXk" firstAttribute="top" secondItem="2Pc-uJ-SAI" secondAttribute="bottom" constant="20" id="0p4-iF-1bx"/> + <constraint firstItem="bu0-90-MB5" firstAttribute="leading" secondItem="E4b-Zv-unB" secondAttribute="leading" id="4YM-7O-C4z"/> + <constraint firstItem="kDG-lW-7DA" firstAttribute="top" secondItem="8TU-za-C7U" secondAttribute="bottom" id="7MN-tF-bVG"/> + <constraint firstItem="8TU-za-C7U" firstAttribute="top" secondItem="Dg0-kS-rT7" secondAttribute="top" id="HvO-Th-xtu"/> + <constraint firstItem="QLK-gs-fOJ" firstAttribute="leading" secondItem="bu0-90-MB5" secondAttribute="leading" id="U37-r7-lNe"/> + <constraint firstItem="E4b-Zv-unB" firstAttribute="top" secondItem="Lbn-cd-UXk" secondAttribute="bottom" constant="8" symbolic="YES" id="XNv-Kr-c8f"/> + <constraint firstItem="bu0-90-MB5" firstAttribute="centerX" secondItem="Dg0-kS-rT7" secondAttribute="centerX" id="YLx-WX-rgL"/> + <constraint firstItem="Lbn-cd-UXk" firstAttribute="centerY" secondItem="Dg0-kS-rT7" secondAttribute="centerY" id="ax2-Za-jO0"/> + <constraint firstItem="QLK-gs-fOJ" firstAttribute="top" secondItem="bu0-90-MB5" secondAttribute="bottom" constant="20" id="cYv-WZ-CCB"/> + <constraint firstItem="QLK-gs-fOJ" firstAttribute="centerX" secondItem="Dg0-kS-rT7" secondAttribute="centerX" id="gQM-Bw-BRN"/> + <constraint firstItem="kDG-lW-7DA" firstAttribute="top" secondItem="11r-JK-mPT" secondAttribute="bottom" id="hKL-lg-yh3"/> + <constraint firstItem="11r-JK-mPT" firstAttribute="leading" secondItem="Dg0-kS-rT7" secondAttribute="leading" id="iUW-Ed-weL"/> + <constraint firstItem="Lbn-cd-UXk" firstAttribute="centerX" secondItem="Dg0-kS-rT7" secondAttribute="centerX" id="l7f-X1-d82"/> + <constraint firstItem="2Pc-uJ-SAI" firstAttribute="centerX" secondItem="Dg0-kS-rT7" secondAttribute="centerX" id="qaX-bX-RjJ"/> + <constraint firstItem="bu0-90-MB5" firstAttribute="trailing" secondItem="E4b-Zv-unB" secondAttribute="trailing" id="rLA-EI-gkd"/> + <constraint firstAttribute="trailing" secondItem="11r-JK-mPT" secondAttribute="trailing" id="shP-7c-cRl"/> + <constraint firstItem="11r-JK-mPT" firstAttribute="top" secondItem="Dg0-kS-rT7" secondAttribute="top" id="sq0-Fa-BSF"/> + <constraint firstItem="8TU-za-C7U" firstAttribute="leading" secondItem="Dg0-kS-rT7" secondAttribute="leading" id="svU-pm-Bc1"/> + <constraint firstItem="QLK-gs-fOJ" firstAttribute="trailing" secondItem="bu0-90-MB5" secondAttribute="trailing" id="uLc-95-Qpb"/> + <constraint firstItem="E4b-Zv-unB" firstAttribute="centerX" secondItem="Dg0-kS-rT7" secondAttribute="centerX" id="ukg-Rd-bL3"/> + <constraint firstAttribute="trailing" secondItem="8TU-za-C7U" secondAttribute="trailing" id="z6w-VC-Pft"/> + </constraints> + </view> + <navigationItem key="navigationItem" id="SVR-Ry-7d7"/> + <connections> + <outlet property="createAccountButton" destination="bu0-90-MB5" id="YNt-Tc-Snc"/> + <outlet property="linkDeviceButton" destination="QLK-gs-fOJ" id="AXP-0r-10g"/> + <outlet property="welcomeTextLabel" destination="E4b-Zv-unB" id="ygV-A3-EZD"/> + <outlet property="welcomeTitleLabel" destination="Lbn-cd-UXk" id="32i-zZ-klu"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="Yc1-qm-cpP" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-333.60000000000002" y="30.134932533733135"/> + </scene> + </scenes> + <resources> + <image name="background_ring" width="750" height="1334"/> + <image name="logo-ring-beta2-blanc" width="180" height="66"/> + </resources> +</document> diff --git a/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.swift b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..30eae6d89d909a84445df5e8ea3e1e8de3a4180c --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewController.swift @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import UIKit +import RxSwift +import RxCocoa +import Reusable + +class WelcomeViewController: UIViewController, StoryboardBased, ViewModelBased { + var viewModel: WelcomeViewModel! + + typealias VMType = WelcomeViewModel + // MARK: outlets + @IBOutlet weak var welcomeTitleLabel: UILabel! + @IBOutlet weak var welcomeTextLabel: UILabel! + @IBOutlet weak var linkDeviceButton: DesignableButton! + @IBOutlet weak var createAccountButton: DesignableButton! + + // MARK: members + private let disposeBag = DisposeBag() + + // MARK: functions + override func viewDidLoad() { + super.viewDidLoad() + + // Bind ViewModel to View + self.viewModel.welcomeTitle.bind(to: self.welcomeTitleLabel.rx.text).disposed(by: self.disposeBag) + self.viewModel.welcomeText.bind(to: self.welcomeTextLabel.rx.text).disposed(by: self.disposeBag) + self.viewModel.createAccount.bind(to: self.createAccountButton.rx.title(for: .normal)).disposed(by: self.disposeBag) + self.viewModel.linkDevice.bind(to: self.linkDeviceButton.rx.title(for: .normal)).disposed(by: self.disposeBag) + + // Bind View Actions to ViewModel + self.createAccountButton.rx.tap.subscribe(onNext: { [unowned self] in + self.viewModel.proceedWithAccountCreation() + }).disposed(by: self.disposeBag) + + self.linkDeviceButton.rx.tap.subscribe(onNext: { [unowned self] in + self.viewModel.proceedWithLinkDevice() + }).disposed(by: self.disposeBag) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: false) + } + +} diff --git a/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewModel.swift b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..991eef164c56a710f7abb4a4f567679171f51ab4 --- /dev/null +++ b/Ring/Ring/Features/Walkthrough/Welcome/WelcomeViewModel.swift @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +class WelcomeViewModel: Stateable, ViewModel { + + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() + + // MARK: - Rx Singles for L10n + let welcomeTitle = Observable<String>.of(L10n.Welcome.title) + let welcomeText = Observable<String>.of(L10n.Welcome.text) + let createAccount = Observable<String>.of(L10n.Welcome.createAccount) + let linkDevice = Observable<String>.of(L10n.Welcome.linkDevice) + + static var count = 0 + + required init (with injectionBag: InjectionBag) { + } + + func proceedWithAccountCreation() { + self.stateSubject.onNext(WalkthroughState.welcomeDone(withType: .createAccount)) + } + + func proceedWithLinkDevice() { + self.stateSubject.onNext(WalkthroughState.welcomeDone(withType: .linkDevice)) + } +} diff --git a/Ring/Ring/Account/AccountConfigModel.swift b/Ring/Ring/Models/AccountConfigModel.swift similarity index 100% rename from Ring/Ring/Account/AccountConfigModel.swift rename to Ring/Ring/Models/AccountConfigModel.swift diff --git a/Ring/Ring/Account/AccountCredentialsModel.swift b/Ring/Ring/Models/AccountCredentialsModel.swift similarity index 100% rename from Ring/Ring/Account/AccountCredentialsModel.swift rename to Ring/Ring/Models/AccountCredentialsModel.swift diff --git a/Ring/Ring/Account/AccountModel.swift b/Ring/Ring/Models/AccountModel.swift similarity index 100% rename from Ring/Ring/Account/AccountModel.swift rename to Ring/Ring/Models/AccountModel.swift diff --git a/Ring/Ring/Account/ConfigKeyModel.swift b/Ring/Ring/Models/ConfigKeyModel.swift similarity index 100% rename from Ring/Ring/Account/ConfigKeyModel.swift rename to Ring/Ring/Models/ConfigKeyModel.swift diff --git a/Ring/Ring/Contacts/ContactModel.swift b/Ring/Ring/Models/ContactModel.swift similarity index 100% rename from Ring/Ring/Contacts/ContactModel.swift rename to Ring/Ring/Models/ContactModel.swift diff --git a/Ring/Ring/Contacts/ContactRequestModel.swift b/Ring/Ring/Models/ContactRequestModel.swift similarity index 100% rename from Ring/Ring/Contacts/ContactRequestModel.swift rename to Ring/Ring/Models/ContactRequestModel.swift diff --git a/Ring/Ring/Conversations/ConversationModel.swift b/Ring/Ring/Models/ConversationModel.swift similarity index 100% rename from Ring/Ring/Conversations/ConversationModel.swift rename to Ring/Ring/Models/ConversationModel.swift diff --git a/Ring/Ring/Account/DeviceModel.swift b/Ring/Ring/Models/DeviceModel.swift similarity index 100% rename from Ring/Ring/Account/DeviceModel.swift rename to Ring/Ring/Models/DeviceModel.swift diff --git a/Ring/Ring/Messages/MessageModel.swift b/Ring/Ring/Models/MessageModel.swift similarity index 100% rename from Ring/Ring/Messages/MessageModel.swift rename to Ring/Ring/Models/MessageModel.swift diff --git a/Ring/Ring/Protocols/Coordinator.swift b/Ring/Ring/Protocols/Coordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..20183c3c10dc0aa0670bf2907c170465df8cfe1b --- /dev/null +++ b/Ring/Ring/Protocols/Coordinator.swift @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import UIKit +import RxSwift + +/** + Represents how a UIViewController should be displayed + + - show: the system adapts the presentation mecanism to fit the context (can be a push in case of a UINavigationViewController for instance) + - present: simply presents the UIViewController + - popup: presents the UIViewController as a modal popup with a .coverVertical transition + */ +public enum PresentationStyle { + case show + case present + case popup +} + +/// A Coordinator drives the navigation of a whole part of the application +protocol Coordinator: class { + + /// the root View Controller to display + var rootViewController: UIViewController { get } + + /// The array containing any child Coordinators + var childCoordinators: [Coordinator] { get set } + + /// Initializes a new Coordinator with a dependancy injection bag + /// + /// - Parameter injectionBag: The injection Bag that will be passed to every sub components that need it + init (with injectionBag: InjectionBag) + + /// Nothing will happen until this function is called + /// it bootstraps the initial UIViewController (after the rooViewController) that will + /// be displayed by this Coordinator + func start () +} + +extension Coordinator { + + /// Adds a child coordinator so that there is a reference to it + /// + /// - Parameter childCoordinator: The coordinator on which we need to keep a reference + func addChildCoordinator(childCoordinator: Coordinator) { + self.childCoordinators.append(childCoordinator) + } + + /// Removes a child coordinator that is no longer used + /// + /// - Parameter childCoordinator: The coordinator we want to remove + func removeChildCoordinator(childCoordinator: Coordinator?) { + guard let child = childCoordinator else { return } + self.childCoordinators = self.childCoordinators.filter { $0 !== child } + } + + /// Present a view controller according to PresentationStyle + /// + /// - Parameters: + /// - viewController: The ViewController we want to present (it will be presented by the rootViewController) + /// - style: The presentation style (show, present or popup) + /// - animation: Wether the transition should be animated or not + func present(viewController: UIViewController, + withStyle style: PresentationStyle, + withAnimation animation: Bool) { + switch style { + case .present: self.rootViewController.present(viewController, + animated: animation, + completion: nil) + break + case .popup: + viewController.modalPresentationStyle = .overCurrentContext + viewController.modalTransitionStyle = .coverVertical + self.rootViewController.present(viewController, + animated: animation, + completion: nil) + break + case .show: self.rootViewController.show(viewController, sender: nil) + break + } + } +} diff --git a/Ring/Ring/Walkthrough/Cell/SwitchCell.swift b/Ring/Ring/Protocols/Stateable.swift similarity index 59% rename from Ring/Ring/Walkthrough/Cell/SwitchCell.swift rename to Ring/Ring/Protocols/Stateable.swift index 8f1ebde24acb13a45b87314d1c0af47c38bf9c0a..0ac56f619cfc954dfee16e954b2c8cc2d211e8da 100644 --- a/Ring/Ring/Walkthrough/Cell/SwitchCell.swift +++ b/Ring/Ring/Protocols/Stateable.swift @@ -1,7 +1,7 @@ /* * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,23 +18,15 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import UIKit -import Reusable +import Foundation +import RxSwift -class SwitchCell: UITableViewCell, NibReusable { - - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var registerSwitch: UISwitch! - - override func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } +/// Represent a navigation state. We mostly use it as an enum +public protocol State { +} +/// A Stateable exposes a navigation state on which StateableResponsive will be subscribed +public protocol Stateable { + /// The state that will be emitted and catch by the StateableResponsive classes to process the navigation + var state: Observable<State> { get } } diff --git a/Ring/Ring/Protocols/StateableResponsive.swift b/Ring/Ring/Protocols/StateableResponsive.swift new file mode 100644 index 0000000000000000000000000000000000000000..61d7f72e36a37eaec97b5d195da97445e4e0532d --- /dev/null +++ b/Ring/Ring/Protocols/StateableResponsive.swift @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import RxSwift + +/// A StateableResponsive can react to Stateable that expose Observable<State> +protocol StateableResponsive { + /// The Rx subject that will be feed by each Stateable + var stateSubject: PublishSubject<State> { get } + + /// The Rx bag that will be used for stateSubject subscriptions + var disposeBag: DisposeBag { get } +} + +extension StateableResponsive where Self: Coordinator { + + /// Present a view controller according to PresentationStyle + /// It also create a subscription between the stateable and the inner stateSubject + /// so that a StateableResponsive can react to state updates + /// + /// - Parameters: + /// - viewController: The ViewController we want to present (it will be presented by the rootViewController) + /// - style: The presentation style (show, present or popup) + /// - animation: Wether the transition should be animated or not + /// - stateable: The stateable the will feed the inner stateSubject + func present(viewController: UIViewController, + withStyle style: PresentationStyle, + withAnimation animation: Bool, + withStateable stateable: Stateable) { + + // present the view controller according to the presentation style + self.present(viewController: viewController, withStyle: style, withAnimation: animation) + + // bind the stateable to the inner state subject + stateable.state.takeUntil(viewController.rx.deallocated).subscribe(onNext: { [weak self] (state) in + self?.stateSubject.onNext(state) + }).disposed(by: self.disposeBag) + } +} diff --git a/Ring/Ring/Protocols/ViewModelBased.swift b/Ring/Ring/Protocols/ViewModelBased.swift new file mode 100644 index 0000000000000000000000000000000000000000..a418694a74aa7cca419238ec43ecb6997b4a0ea5 --- /dev/null +++ b/Ring/Ring/Protocols/ViewModelBased.swift @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import Foundation +import Reusable +import UIKit + +/// We assume that every application ViewModel should be aware of the injection bag +/// it allows the factorize a ViewModelBased UIViewController instantiation +protocol ViewModel: class { + + /// Initializes a new ViewModel with a dependancy injection bag + /// + /// - Parameter injectionBag: The injection Bag that will be passed to every sub components that need it + init(with injectionBag: InjectionBag) +} + +protocol ViewModelBased: class { + associatedtype VMType: ViewModel + + /// The viewModel that will be automagically instantiated by instantiate(with injectionBag: InjectionBag) + var viewModel: VMType! { get set } +} + +extension ViewModelBased where Self: UIViewController, Self: StoryboardBased { + + /// Initializes a new ViewModelBased UIViewController + /// The associated ViewModel will be instantiated as well + /// + /// - Parameter injectionBag: The injection Bag that will be passed to every sub components that need it + /// - Returns: The ViewModelBased UIViewController with its inner ViewModel already instantiated + static func instantiate(with injectionBag: InjectionBag) -> Self { + let viewController = Self.instantiate() + viewController.viewModel = VMType(with: injectionBag) + return viewController + } +} diff --git a/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..8266831dc5ca733668136647c9f0153c4f163c98 --- /dev/null +++ b/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "background_ring.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/background_ring.png b/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/background_ring.png new file mode 100644 index 0000000000000000000000000000000000000000..afc68a862412635aeef5c4bd48ec243b406fadf2 Binary files /dev/null and b/Ring/Ring/Resources/Images.xcassets/background_ring.imageset/background_ring.png differ diff --git a/Ring/Ring/Resources/Main.storyboard b/Ring/Ring/Resources/Main.storyboard deleted file mode 100644 index a88652428d9af6b9a2cb55c1f659f2a1ada658fa..0000000000000000000000000000000000000000 --- a/Ring/Ring/Resources/Main.storyboard +++ /dev/null @@ -1,479 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="qdG-Sd-QaE"> - <device id="retina4_0" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/> - <capability name="Constraints to layout margins" minToolsVersion="6.0"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <scenes> - <!--Me--> - <scene sceneID="enX-SQ-9q5"> - <objects> - <viewController title="Me" id="Adn-sB-m3h" customClass="MeViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="pI3-q9-zKr"/> - <viewControllerLayoutGuide type="bottom" id="37a-rZ-qQK"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="5d9-Wo-lnI"> - <rect key="frame" x="0.0" y="64" width="320" height="504"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="PYw-q1-MgO"> - <rect key="frame" x="105" y="85" width="110" height="110"/> - <constraints> - <constraint firstAttribute="width" constant="110" id="JL8-2N-trX"/> - <constraint firstAttribute="height" constant="110" id="sfn-3N-dGV"/> - </constraints> - </imageView> - <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="w6i-WA-dDu"> - <rect key="frame" x="0.0" y="237" width="320" height="218"/> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <constraints> - <constraint firstAttribute="height" constant="218" id="1Nb-4o-17J"/> - </constraints> - <prototypes> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="AccountTableViewCell" id="i1O-Yc-WGd" customClass="AccountTableViewCell" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="0.0" y="28" width="320" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="i1O-Yc-WGd" id="Bz1-A3-Z3f"> - <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bm0-lC-K2F"> - <rect key="frame" x="8" y="6" width="51" height="31"/> - <connections> - <action selector="switchAccountState:" destination="i1O-Yc-WGd" eventType="valueChanged" id="ldw-sU-Bm1"/> - </connections> - </switch> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="TYPE" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aWK-hB-xaJ"> - <rect key="frame" x="230" y="11" width="42" height="21"/> - <constraints> - <constraint firstAttribute="width" constant="42" id="ow1-lO-Jjx"/> - </constraints> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="di5-bP-pFg" userLabel="Account Name Label"> - <rect key="frame" x="71" y="11" width="100" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <constraints> - <constraint firstItem="aWK-hB-xaJ" firstAttribute="baseline" secondItem="di5-bP-pFg" secondAttribute="baseline" id="9dW-hI-rzm"/> - <constraint firstItem="aWK-hB-xaJ" firstAttribute="leading" secondItem="di5-bP-pFg" secondAttribute="trailing" constant="59" id="MRQ-i6-gY5"/> - <constraint firstItem="bm0-lC-K2F" firstAttribute="centerY" secondItem="Bz1-A3-Z3f" secondAttribute="centerY" id="PNk-Oe-B7N"/> - <constraint firstItem="bm0-lC-K2F" firstAttribute="leading" secondItem="Bz1-A3-Z3f" secondAttribute="leadingMargin" id="RRg-JS-FGF"/> - <constraint firstAttribute="trailingMargin" secondItem="aWK-hB-xaJ" secondAttribute="trailing" constant="40" id="te2-La-m4K"/> - <constraint firstItem="di5-bP-pFg" firstAttribute="leading" secondItem="bm0-lC-K2F" secondAttribute="trailing" constant="14" id="uw0-SR-JTt"/> - <constraint firstItem="bm0-lC-K2F" firstAttribute="centerY" secondItem="di5-bP-pFg" secondAttribute="centerY" id="vK9-0E-f3s"/> - </constraints> - </tableViewCellContentView> - <connections> - <outlet property="accountNameLabel" destination="di5-bP-pFg" id="Mcx-HF-rF2"/> - <outlet property="accountTypeLabel" destination="aWK-hB-xaJ" id="pca-qt-irG"/> - <outlet property="activeSwitch" destination="bm0-lC-K2F" id="khb-tT-oXy"/> - <segue destination="bXM-3w-ypq" kind="show" identifier="accountDetails" id="cPm-ET-ZGJ"/> - </connections> - </tableViewCell> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="addAccountTableCell" id="Ok2-8L-eMm"> - <rect key="frame" x="0.0" y="72" width="320" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Ok2-8L-eMm" id="m64-AI-t2h"> - <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kZv-uf-BsD" userLabel="Account Name Label"> - <rect key="frame" x="72" y="11" width="99" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="contactAdd" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aNy-Wu-wDj"> - <rect key="frame" x="23" y="11" width="22" height="22"/> - <connections> - <action selector="addAccountClicked:" destination="Adn-sB-m3h" eventType="touchUpInside" id="qSe-cZ-2Yl"/> - </connections> - </button> - </subviews> - <constraints> - <constraint firstItem="kZv-uf-BsD" firstAttribute="top" secondItem="aNy-Wu-wDj" secondAttribute="top" id="IvY-99-bq2"/> - <constraint firstItem="aNy-Wu-wDj" firstAttribute="leading" secondItem="m64-AI-t2h" secondAttribute="leadingMargin" constant="15" id="nYT-zW-vmf"/> - <constraint firstItem="aNy-Wu-wDj" firstAttribute="centerY" secondItem="m64-AI-t2h" secondAttribute="centerY" id="nbd-rJ-QzV"/> - <constraint firstItem="kZv-uf-BsD" firstAttribute="leading" secondItem="aNy-Wu-wDj" secondAttribute="trailing" constant="27" id="neg-sx-5EB"/> - </constraints> - </tableViewCellContentView> - </tableViewCell> - </prototypes> - <connections> - <outlet property="dataSource" destination="Adn-sB-m3h" id="ffX-xl-OWf"/> - <outlet property="delegate" destination="Adn-sB-m3h" id="paE-u4-bF2"/> - </connections> - </tableView> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PERSON PLACEHOLDER" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vcL-Nj-NcH"> - <rect key="frame" x="16" y="20" width="180" height="64"/> - <constraints> - <constraint firstAttribute="width" constant="180" id="CUM-z9-ubY"/> - <constraint firstAttribute="height" constant="64" id="qTZ-Qf-EMe"/> - </constraints> - <fontDescription key="fontDescription" type="system" pointSize="22"/> - <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <constraints> - <constraint firstItem="w6i-WA-dDu" firstAttribute="top" secondItem="PYw-q1-MgO" secondAttribute="bottom" constant="42" id="01d-Gb-x0d"/> - <constraint firstAttribute="trailing" secondItem="w6i-WA-dDu" secondAttribute="trailing" id="EiT-gL-H1H"/> - <constraint firstItem="w6i-WA-dDu" firstAttribute="leading" secondItem="5d9-Wo-lnI" secondAttribute="leading" id="JAh-ny-3hX"/> - <constraint firstItem="37a-rZ-qQK" firstAttribute="top" secondItem="w6i-WA-dDu" secondAttribute="bottom" id="Kgu-hy-eYU"/> - <constraint firstItem="w6i-WA-dDu" firstAttribute="centerX" secondItem="PYw-q1-MgO" secondAttribute="centerX" id="NhE-xV-NPT"/> - <constraint firstItem="vcL-Nj-NcH" firstAttribute="leading" secondItem="5d9-Wo-lnI" secondAttribute="leadingMargin" id="k8C-mb-o4x"/> - <constraint firstItem="vcL-Nj-NcH" firstAttribute="top" secondItem="pI3-q9-zKr" secondAttribute="bottom" constant="20" id="rrl-Pm-vva"/> - </constraints> - </view> - <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> - <tabBarItem key="tabBarItem" title="Me" id="K9k-MS-meT"/> - <navigationItem key="navigationItem" id="ncw-EB-hXe"> - <barButtonItem key="rightBarButtonItem" systemItem="edit" id="UKS-Qs-jTc"/> - </navigationItem> - <connections> - <outlet property="accountTableView" destination="w6i-WA-dDu" id="XVl-nJ-raI"/> - <outlet property="nameLabel" destination="vcL-Nj-NcH" id="Wv8-4W-bQR"/> - <outlet property="qrImageView" destination="PYw-q1-MgO" id="CLv-aw-aI4"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="e9Q-pi-QIS" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-94" y="-438"/> - </scene> - <!--Details--> - <scene sceneID="EYO-6b-v34"> - <objects> - <viewController title="Details" id="bXM-3w-ypq" customClass="AccountDetailsViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="4Js-Hd-F7A"/> - <viewControllerLayoutGuide type="bottom" id="1SM-kJ-lna"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="uEm-4u-10U"> - <rect key="frame" x="0.0" y="64" width="320" height="504"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="clip" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.25" translatesAutoresizingMaskIntoConstraints="NO" id="fEM-NK-AEk"> - <rect key="frame" x="16" y="20" width="288" height="16"/> - <fontDescription key="fontDescription" type="system" pointSize="13"/> - <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <constraints> - <constraint firstItem="fEM-NK-AEk" firstAttribute="trailing" secondItem="uEm-4u-10U" secondAttribute="trailingMargin" id="LjA-mF-o27"/> - <constraint firstItem="fEM-NK-AEk" firstAttribute="leading" secondItem="uEm-4u-10U" secondAttribute="leadingMargin" id="XZm-Fg-mcn"/> - <constraint firstItem="fEM-NK-AEk" firstAttribute="top" secondItem="4Js-Hd-F7A" secondAttribute="bottom" constant="20" id="jFe-Ha-jzG"/> - </constraints> - </view> - <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> - <navigationItem key="navigationItem" title="Details" id="Pno-a9-G1I"/> - <connections> - <outlet property="detailsLabel" destination="fEM-NK-AEk" id="nCN-kQ-UTS"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="HG4-2x-M68" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="784" y="-438"/> - </scene> - <!--Me--> - <scene sceneID="zvS-DX-8kB"> - <objects> - <navigationController id="acv-jH-RCt" sceneMemberID="viewController"> - <tabBarItem key="tabBarItem" title="Me" id="jo3-1i-bFH"/> - <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="tAT-cg-hut"> - <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <color key="barTintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - </navigationBar> - <connections> - <segue destination="Adn-sB-m3h" kind="relationship" relationship="rootViewController" id="yoa-Sb-o2W"/> - </connections> - </navigationController> - <placeholder placeholderIdentifier="IBFirstResponder" id="5pC-Na-ca9" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-954" y="-438"/> - </scene> - <!--Contact Requests View Controller--> - <scene sceneID="Ycd-tS-NHq"> - <objects> - <viewController id="Lci-Jw-hcF" customClass="ContactRequestsViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="pbg-vL-UEw"/> - <viewControllerLayoutGuide type="bottom" id="M61-3o-lad"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="8xJ-iz-wpS"> - <rect key="frame" x="0.0" y="64" width="320" height="504"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Mua-zi-Icu"> - <rect key="frame" x="0.0" y="0.0" width="320" height="455"/> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </tableView> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qOH-GE-nEf"> - <rect key="frame" x="0.0" y="0.0" width="320" height="455"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No invitations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="s8R-RG-BKE"> - <rect key="frame" x="107.5" y="216.5" width="104" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="s8R-RG-BKE" firstAttribute="centerX" secondItem="qOH-GE-nEf" secondAttribute="centerX" id="Elc-yW-4xl"/> - <constraint firstItem="s8R-RG-BKE" firstAttribute="centerY" secondItem="qOH-GE-nEf" secondAttribute="centerY" id="eqa-R7-T4b"/> - </constraints> - </view> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstAttribute="trailing" secondItem="qOH-GE-nEf" secondAttribute="trailing" id="0AB-6r-1uc"/> - <constraint firstItem="Mua-zi-Icu" firstAttribute="leading" secondItem="8xJ-iz-wpS" secondAttribute="leading" id="8nJ-KD-RHV"/> - <constraint firstItem="M61-3o-lad" firstAttribute="top" secondItem="Mua-zi-Icu" secondAttribute="bottom" id="IWw-rS-BG5"/> - <constraint firstItem="qOH-GE-nEf" firstAttribute="leading" secondItem="8xJ-iz-wpS" secondAttribute="leading" id="ZVa-7c-RRC"/> - <constraint firstItem="M61-3o-lad" firstAttribute="top" secondItem="qOH-GE-nEf" secondAttribute="bottom" id="dHT-QA-V8l"/> - <constraint firstItem="qOH-GE-nEf" firstAttribute="top" secondItem="pbg-vL-UEw" secondAttribute="bottom" id="dqQ-DP-Hzp"/> - <constraint firstAttribute="trailing" secondItem="Mua-zi-Icu" secondAttribute="trailing" id="iXE-04-OvM"/> - <constraint firstItem="Mua-zi-Icu" firstAttribute="top" secondItem="pbg-vL-UEw" secondAttribute="bottom" id="xbv-ZV-ieJ"/> - </constraints> - </view> - <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> - <navigationItem key="navigationItem" id="Iw5-ES-jAu"/> - <connections> - <outlet property="noInvitationsPlaceholder" destination="qOH-GE-nEf" id="fIP-fm-d8A"/> - <outlet property="tableView" destination="Mua-zi-Icu" id="5wQ-xv-AvD"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="IzE-h9-VPi" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-51" y="351"/> - </scene> - <!--Invitations--> - <scene sceneID="Ed7-lZ-cjo"> - <objects> - <navigationController id="Xrg-sU-NZU" sceneMemberID="viewController"> - <tabBarItem key="tabBarItem" title="Invitations" id="zsX-e6-y8k"/> - <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="AhC-gp-TdW"> - <rect key="frame" x="0.0" y="0.0" width="375" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <color key="barTintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - </navigationBar> - <connections> - <segue destination="Lci-Jw-hcF" kind="relationship" relationship="rootViewController" id="mtd-lE-Yms"/> - </connections> - </navigationController> - <placeholder placeholderIdentifier="IBFirstResponder" id="BrT-9A-2LC" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-954" y="351"/> - </scene> - <!--Home--> - <scene sceneID="RrV-5t-V1t"> - <objects> - <navigationController id="m3Y-hs-SzS" sceneMemberID="viewController"> - <tabBarItem key="tabBarItem" title="Home" id="vc8-sv-IF0"/> - <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="G6y-tt-W3p"> - <rect key="frame" x="0.0" y="0.0" width="375" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <color key="barTintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - </navigationBar> - <connections> - <segue destination="NIj-Cd-aWO" kind="relationship" relationship="rootViewController" id="GUx-m9-dbP"/> - </connections> - </navigationController> - <placeholder placeholderIdentifier="IBFirstResponder" id="GAd-lM-wsI" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-954" y="-1178"/> - </scene> - <!--Home--> - <scene sceneID="UuZ-iE-GB0"> - <objects> - <viewController title="Home" id="NIj-Cd-aWO" customClass="SmartlistViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="s1e-Lp-B2j"/> - <viewControllerLayoutGuide type="bottom" id="aoH-Yk-Qrn"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="khr-49-0iv"> - <rect key="frame" x="0.0" y="64" width="320" height="504"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <searchBar contentMode="redraw" placeholder="Enter name..." translatesAutoresizingMaskIntoConstraints="NO" id="uCX-a5-egQ"> - <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <color key="tintColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> - <offsetWrapper key="searchFieldBackgroundPositionAdjustment" horizontal="0.0" vertical="0.0"/> - <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="namePhonePad" returnKeyType="done"/> - <connections> - <outlet property="delegate" destination="NIj-Cd-aWO" id="nrQ-TN-RFw"/> - </connections> - </searchBar> - <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="B6Y-MZ-L7L"> - <rect key="frame" x="0.0" y="44" width="320" height="411"/> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - </tableView> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aeB-7A-alJ"> - <rect key="frame" x="0.0" y="44" width="320" height="411"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No conversations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tuk-H4-adP"> - <rect key="frame" x="94" y="195" width="133" height="21"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="Tuk-H4-adP" firstAttribute="centerY" secondItem="aeB-7A-alJ" secondAttribute="centerY" id="DWU-Br-H6b"/> - <constraint firstItem="Tuk-H4-adP" firstAttribute="centerX" secondItem="aeB-7A-alJ" secondAttribute="centerX" id="EhK-Zq-lbK"/> - </constraints> - </view> - <tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="1pl-Jb-V2A"> - <rect key="frame" x="0.0" y="44" width="320" height="411"/> - <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> - <label key="tableHeaderView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IQr-J3-Dyb"> - <rect key="frame" x="0.0" y="0.0" width="320" height="24"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <fontDescription key="fontDescription" type="system" pointSize="14"/> - <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </tableView> - </subviews> - <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <constraints> - <constraint firstItem="1pl-Jb-V2A" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="0kW-Z4-4kE"/> - <constraint firstAttribute="trailing" secondItem="aeB-7A-alJ" secondAttribute="trailing" id="6lb-NM-Kv1"/> - <constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="B6Y-MZ-L7L" secondAttribute="bottom" id="8dr-z2-qYT"/> - <constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="1pl-Jb-V2A" secondAttribute="bottom" id="DRW-kt-vzQ"/> - <constraint firstItem="B6Y-MZ-L7L" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="I1L-uJ-KI4"/> - <constraint firstItem="1pl-Jb-V2A" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="N3f-tu-YZd"/> - <constraint firstItem="B6Y-MZ-L7L" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="QM6-tE-fRQ"/> - <constraint firstAttribute="trailing" secondItem="uCX-a5-egQ" secondAttribute="trailing" id="XcJ-y7-DKa"/> - <constraint firstAttribute="trailing" secondItem="B6Y-MZ-L7L" secondAttribute="trailing" id="b7u-DV-d1S"/> - <constraint firstItem="uCX-a5-egQ" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="bB2-ML-Tck"/> - <constraint firstItem="aeB-7A-alJ" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="dWi-wQ-oVH"/> - <constraint firstItem="uCX-a5-egQ" firstAttribute="top" secondItem="s1e-Lp-B2j" secondAttribute="bottom" id="sBi-PG-yh3"/> - <constraint firstItem="aeB-7A-alJ" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="wlD-ZX-Pgo"/> - <constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="aeB-7A-alJ" secondAttribute="bottom" id="zub-RB-pYy"/> - <constraint firstAttribute="trailing" secondItem="1pl-Jb-V2A" secondAttribute="trailing" id="zul-Kh-urI"/> - </constraints> - </view> - <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> - <tabBarItem key="tabBarItem" title="Home" id="1QA-0Y-BFL"/> - <navigationItem key="navigationItem" id="b8m-eG-Q9D"/> - <connections> - <outlet property="conversationsTableView" destination="B6Y-MZ-L7L" id="1qp-yP-v0E"/> - <outlet property="noConversationsView" destination="aeB-7A-alJ" id="LsS-ch-rh0"/> - <outlet property="searchBar" destination="uCX-a5-egQ" id="xsm-gp-Yjb"/> - <outlet property="searchResultsTableView" destination="1pl-Jb-V2A" id="Ywb-Lm-6S7"/> - <outlet property="searchTableViewLabel" destination="IQr-J3-Dyb" id="wOe-wg-q30"/> - <outlet property="tableView" destination="B6Y-MZ-L7L" id="dXp-J4-x68"/> - <segue destination="Qlv-cA-wRT" kind="show" identifier="ShowMessages" id="X75-kM-dPZ"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="rzQ-ll-5bo" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-97.5" y="-1177.8169014084508"/> - </scene> - <!--Conversation View Controller--> - <scene sceneID="N9T-Vl-P5n"> - <objects> - <viewController hidesBottomBarWhenPushed="YES" id="Qlv-cA-wRT" customClass="ConversationViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="wEb-Zj-bvJ"/> - <viewControllerLayoutGuide type="bottom" id="S9d-I1-nWj"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="jPi-CC-dFO"> - <rect key="frame" x="0.0" y="64" width="320" height="455"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="M1r-X5-oFv"> - <rect key="frame" x="0.0" y="0.0" width="320" height="455"/> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <prototypes> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MessageCell" id="8rX-Qa-Ypu"> - <rect key="frame" x="0.0" y="28" width="320" height="44"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8rX-Qa-Ypu" id="7vA-nx-B3h"> - <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/> - <autoresizingMask key="autoresizingMask"/> - </tableViewCellContentView> - </tableViewCell> - </prototypes> - </tableView> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="PYu-2x-JXM"> - <rect key="frame" x="0.0" y="0.0" width="320" height="455"/> - <subviews> - <activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="oH9-4M-JpG"> - <rect key="frame" x="142" y="209" width="37" height="37"/> - <color key="color" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/> - </activityIndicatorView> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="oH9-4M-JpG" firstAttribute="centerY" secondItem="PYu-2x-JXM" secondAttribute="centerY" id="105-id-Oqu"/> - <constraint firstItem="oH9-4M-JpG" firstAttribute="centerX" secondItem="PYu-2x-JXM" secondAttribute="centerX" id="Fk6-H6-Vdq"/> - </constraints> - </view> - </subviews> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstAttribute="trailing" secondItem="M1r-X5-oFv" secondAttribute="trailing" id="4Dh-nK-p84"/> - <constraint firstItem="M1r-X5-oFv" firstAttribute="top" secondItem="jPi-CC-dFO" secondAttribute="top" id="BFo-2E-BaG"/> - <constraint firstAttribute="bottom" secondItem="M1r-X5-oFv" secondAttribute="bottom" id="PoM-IX-CFt"/> - <constraint firstAttribute="trailing" secondItem="PYu-2x-JXM" secondAttribute="trailing" id="X7H-Er-Udk"/> - <constraint firstItem="PYu-2x-JXM" firstAttribute="leading" secondItem="jPi-CC-dFO" secondAttribute="leading" id="dQk-7P-cld"/> - <constraint firstItem="PYu-2x-JXM" firstAttribute="top" secondItem="jPi-CC-dFO" secondAttribute="top" id="g9s-sd-GJC"/> - <constraint firstItem="M1r-X5-oFv" firstAttribute="leading" secondItem="jPi-CC-dFO" secondAttribute="leading" id="i0E-9E-6co"/> - <constraint firstAttribute="bottom" secondItem="PYu-2x-JXM" secondAttribute="bottom" id="j3b-gd-iBK"/> - </constraints> - </view> - <extendedEdge key="edgesForExtendedLayout"/> - <connections> - <outlet property="spinnerView" destination="PYu-2x-JXM" id="hxZ-lU-3XC"/> - <outlet property="tableView" destination="M1r-X5-oFv" id="5Lh-Iu-nQb"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="bv6-qf-2Pa" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="844" y="-1179"/> - </scene> - <!--Ring--> - <scene sceneID="oqo-zJ-m0o"> - <objects> - <tabBarController storyboardIdentifier="MainStoryboard" title="Ring" id="qdG-Sd-QaE" customClass="MainTabBarViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <tabBar key="tabBar" contentMode="scaleToFill" id="zN5-xb-CQh"> - <rect key="frame" x="0.0" y="0.0" width="320" height="49"/> - <autoresizingMask key="autoresizingMask"/> - <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> - <color key="tintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - </tabBar> - <connections> - <segue destination="m3Y-hs-SzS" kind="relationship" relationship="viewControllers" id="tnG-cB-lXh"/> - <segue destination="Xrg-sU-NZU" kind="relationship" relationship="viewControllers" id="mPy-yr-7oa"/> - <segue destination="acv-jH-RCt" kind="relationship" relationship="viewControllers" id="0f1-MJ-FH1"/> - </connections> - </tabBarController> - <placeholder placeholderIdentifier="IBFirstResponder" id="JF8-3k-Wf9" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-1948" y="-839"/> - </scene> - </scenes> -</document> diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings index 268b65288b8a0e6191309516cf40daf96f7c75ec..1735727532d37a56ea8ef42b307ad288c09fb1f6 100644 --- a/Ring/Ring/Resources/en.lproj/Localizable.strings +++ b/Ring/Ring/Resources/en.lproj/Localizable.strings @@ -20,6 +20,7 @@ // Global "global.homeTabBarTitle" = "Home"; +"global.contactRequestsTabBarTitle" = "Invitations"; "global.meTabBarTitle" = "Me"; "global.ok" = "Ok"; @@ -34,25 +35,26 @@ //Welcome Screen "welcome.title" = "Welcome to Ring"; -"welcome.text" = "A Ring account allows you to reach people securely in peer to peer through fully distributed network"; -"welcome.linkDeviceButton" = "Link this device to an account"; +"welcome.text" = "Ring is a free and universal communication platform which preserves the users' privacy and freedoms"; +"welcome.linkDevice" = "Link this device to an account"; "welcome.createAccount" = "Create a Ring account"; +//Creation Profile Screen +"createProfile.createAccount" = "Skip to Create Account"; +"createProfile.linkDevice" = "Skip to Link Device"; + //Create Account form "createAccount.createAccountFormTitle" = "Create your Ring account"; -"createAccount.registerPublicUsername" = "Register public username (experimental)"; "createAccount.chooseStrongPassword" = "Choose strong password you will remember to protect your Ring account."; -"createAccount.enterNewUsernamePlaceholder" = "Enter new username"; -"createAccount.newPasswordPlaceholder" = "New Password"; -"createAccount.repeatPasswordPlaceholder" = "Repeat new password"; +"createAccount.enterNewUsernamePlaceholder" = "username"; +"createAccount.newPasswordPlaceholder" = "password"; +"createAccount.repeatPasswordPlaceholder" = "confirm password"; "createAccount.passwordCharactersNumberError" = "6 characters minimum"; -"createAccount.passwordNotMatchingError" = "Passwords do not match"; -"createAccount.lookingForUsernameAvailability" = "Looking for username availability..."; -"createAccount.invalidUsername" = "Invalid username"; -"createAccount.usernameAlreadyTaken" = "Username already taken"; -"createAccount.loading" = "Loading..."; - -//Progress View +"createAccount.passwordNotMatchingError" = "passwords do not match"; +"createAccount.lookingForUsernameAvailability" = "looking for username availability"; +"createAccount.invalidUsername" = "invalid username"; +"createAccount.usernameAlreadyTaken" = "username already taken"; +"createAccount.loading" = "Loading"; "createAccount.waitCreateAccountTitle" = "Adding account"; //Alerts diff --git a/Ring/Ring/Services/ContactsService.swift b/Ring/Ring/Services/ContactsService.swift index 09a23e638c2e5bf0611b7054d73d7dbc0511328d..60ef462125ddafde3e57dbef85410cdae9db914e 100644 --- a/Ring/Ring/Services/ContactsService.swift +++ b/Ring/Ring/Services/ContactsService.swift @@ -90,7 +90,7 @@ class ContactsService { func accept(contactRequest: ContactRequestModel, withAccount account: AccountModel) -> Observable<Void> { return Observable.create { [unowned self] observable in let success = self.contactsAdapter.acceptTrustRequest(fromContact: contactRequest.ringId, - withAccountId: account.id) + withAccountId: account.id) if success { observable.on(.completed) } else { diff --git a/Ring/Ring/UI/DesignableButton.swift b/Ring/Ring/UI/DesignableButton.swift index ad771dd494a0522aa7d487f63156f9ede123fcd5..bdcaa9c68e2761331f38828b03fcf7bcbc56784f 100644 --- a/Ring/Ring/UI/DesignableButton.swift +++ b/Ring/Ring/UI/DesignableButton.swift @@ -1,7 +1,7 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/Ring/Ring/UI/DesignableTextField.swift b/Ring/Ring/UI/DesignableTextField.swift index 7e7f2da2fd8382c95b103d81b78fdaf0bdcdc81a..f0a6922eed8be4ae9923bc41d2508b448685896b 100644 --- a/Ring/Ring/UI/DesignableTextField.swift +++ b/Ring/Ring/UI/DesignableTextField.swift @@ -1,7 +1,7 @@ /* * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/Ring/Ring/MainTabBar/MainTabBarViewController.swift b/Ring/Ring/UI/DesignableView.swift similarity index 65% rename from Ring/Ring/MainTabBar/MainTabBarViewController.swift rename to Ring/Ring/UI/DesignableView.swift index db89f9dc4d86d3033ee0054f7f50784bf548ef7e..60de59bb28bf5b9fe10c609ff7532437013f1244 100644 --- a/Ring/Ring/MainTabBar/MainTabBarViewController.swift +++ b/Ring/Ring/UI/DesignableView.swift @@ -1,7 +1,7 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * - * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> + * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,15 +20,15 @@ import UIKit -class MainTabBarViewController: UITabBarController { - fileprivate let accountService = AppDelegate.accountService +@IBDesignable +class DesignableView: UIView { + // just to make the UIView+Ring extension IBDesignable + override func layoutSubviews() { + super.layoutSubviews() - override func viewDidLoad() { - super.viewDidLoad() + if let layer = self.layer.sublayers?[0] as? CAGradientLayer { + layer.frame = self.frame + } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - } } diff --git a/Ring/Ring/Messages/MessableBubble.swift b/Ring/Ring/UI/MessableBubble.swift similarity index 95% rename from Ring/Ring/Messages/MessableBubble.swift rename to Ring/Ring/UI/MessableBubble.swift index 1ce5f3849ea2f346298c1c5aa972945ff6c94ae1..d1b989d18bb4c2625fec7cb0e14b89fcf7bdd7e9 100644 --- a/Ring/Ring/Messages/MessableBubble.swift +++ b/Ring/Ring/UI/MessableBubble.swift @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Savoir-faire Linux Inc. + * Copyright (C) 2017 Savoir-faire Linux Inc. * * Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com> * diff --git a/Ring/Ring/Walkthrough/Cell/SwitchCell.xib b/Ring/Ring/Walkthrough/Cell/SwitchCell.xib deleted file mode 100644 index d659bb2c53325fc5be1ffc1775faa04770d6a8d9..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/Cell/SwitchCell.xib +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> - <device id="retina4_7" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <objects> - <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> - <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="69" id="Yif-IF-81k" customClass="SwitchCell" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="320" height="69"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Yif-IF-81k" id="EOF-e2-nje"> - <rect key="frame" x="0.0" y="0.0" width="320" height="68.5"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Register public username (experimental)" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tA2-KH-hQH"> - <rect key="frame" x="16" y="8" width="231" height="53"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mew-RY-xpg"> - <rect key="frame" x="255" y="19" width="51" height="31"/> - </switch> - </subviews> - <constraints> - <constraint firstItem="mew-RY-xpg" firstAttribute="centerY" secondItem="EOF-e2-nje" secondAttribute="centerY" id="IPn-AW-SF6"/> - <constraint firstAttribute="bottom" secondItem="tA2-KH-hQH" secondAttribute="bottom" constant="8" id="OTV-UN-GEE"/> - <constraint firstItem="mew-RY-xpg" firstAttribute="leading" secondItem="tA2-KH-hQH" secondAttribute="trailing" constant="8" id="We4-Xh-xDG"/> - <constraint firstAttribute="trailing" secondItem="mew-RY-xpg" secondAttribute="trailing" constant="16" id="Xlc-0g-krk"/> - <constraint firstItem="tA2-KH-hQH" firstAttribute="top" secondItem="EOF-e2-nje" secondAttribute="top" constant="8" id="gL7-Zb-Jzg"/> - <constraint firstItem="tA2-KH-hQH" firstAttribute="leading" secondItem="EOF-e2-nje" secondAttribute="leading" constant="16" id="qE8-OH-OlE"/> - </constraints> - </tableViewCellContentView> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <connections> - <outlet property="registerSwitch" destination="mew-RY-xpg" id="FEC-Ep-LTP"/> - <outlet property="titleLabel" destination="tA2-KH-hQH" id="eJz-g7-ECA"/> - </connections> - </tableViewCell> - </objects> -</document> diff --git a/Ring/Ring/Walkthrough/Cell/TextCell.xib b/Ring/Ring/Walkthrough/Cell/TextCell.xib deleted file mode 100644 index 28b17d414f58b05e3d7bbca1ffe64b82ca18b397..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/Cell/TextCell.xib +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> - <device id="retina4_7" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <objects> - <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> - <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="69" id="6fR-7o-uTx" customClass="TextCell" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="320" height="69"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="6fR-7o-uTx" id="dja-bC-isJ"> - <rect key="frame" x="0.0" y="0.0" width="320" height="69"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jFA-AW-xcV"> - <rect key="frame" x="16" y="8" width="288" height="53"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <constraints> - <constraint firstAttribute="bottom" secondItem="jFA-AW-xcV" secondAttribute="bottom" constant="8" id="1gA-Tq-G8N"/> - <constraint firstItem="jFA-AW-xcV" firstAttribute="leading" secondItem="dja-bC-isJ" secondAttribute="leading" constant="16" id="5uH-dG-rpe"/> - <constraint firstAttribute="trailing" secondItem="jFA-AW-xcV" secondAttribute="trailing" constant="16" id="DcD-Zr-TSr"/> - <constraint firstItem="jFA-AW-xcV" firstAttribute="top" secondItem="dja-bC-isJ" secondAttribute="top" constant="8" id="RM5-V7-gpN"/> - </constraints> - </tableViewCellContentView> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <connections> - <outlet property="label" destination="jFA-AW-xcV" id="2kT-f6-q4f"/> - </connections> - </tableViewCell> - </objects> -</document> diff --git a/Ring/Ring/Walkthrough/Cell/TextFieldCell.xib b/Ring/Ring/Walkthrough/Cell/TextFieldCell.xib deleted file mode 100644 index 56900cfe05aa9f5ca90d93728474b3d8e02567d2..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/Cell/TextFieldCell.xib +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> - <device id="retina4_7" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <objects> - <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> - <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> - <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="80" id="U14-3B-dOe" customClass="TextFieldCell" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="320" height="80"/> - <autoresizingMask key="autoresizingMask"/> - <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="U14-3B-dOe" id="4gJ-Ji-yr3"> - <rect key="frame" x="0.0" y="0.0" width="320" height="79.5"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="let-RU-Vya" customClass="DesignableTextField" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="16" y="8" width="288" height="42"/> - <nil key="textColor"/> - <fontDescription key="fontDescription" type="system" pointSize="14"/> - <textInputTraits key="textInputTraits" secureTextEntry="YES"/> - </textField> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JXp-TT-tN0"> - <rect key="frame" x="16" y="54" width="288" height="17"/> - <constraints> - <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="17" id="EVv-1r-8dZ"/> - </constraints> - <fontDescription key="fontDescription" type="system" pointSize="14"/> - <color key="textColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <constraints> - <constraint firstAttribute="trailing" secondItem="let-RU-Vya" secondAttribute="trailing" constant="16" id="8tN-FD-abO"/> - <constraint firstItem="let-RU-Vya" firstAttribute="top" secondItem="4gJ-Ji-yr3" secondAttribute="top" constant="8" id="CA5-jm-57y"/> - <constraint firstItem="JXp-TT-tN0" firstAttribute="leading" secondItem="4gJ-Ji-yr3" secondAttribute="leading" constant="16" id="NpS-VI-iDF"/> - <constraint firstAttribute="bottom" secondItem="JXp-TT-tN0" secondAttribute="bottom" constant="8" id="lWN-0g-Bd1"/> - <constraint firstItem="let-RU-Vya" firstAttribute="leading" secondItem="4gJ-Ji-yr3" secondAttribute="leading" constant="16" id="rYs-8Q-y71"/> - <constraint firstItem="JXp-TT-tN0" firstAttribute="top" secondItem="let-RU-Vya" secondAttribute="bottom" constant="4" id="wWh-Vn-WVa"/> - <constraint firstAttribute="trailing" secondItem="JXp-TT-tN0" secondAttribute="trailing" constant="16" id="xfk-Qg-XAg"/> - </constraints> - </tableViewCellContentView> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <connections> - <outlet property="errorMessageLabel" destination="JXp-TT-tN0" id="6ar-LE-Hd5"/> - <outlet property="textField" destination="let-RU-Vya" id="mSE-wJ-u6p"/> - </connections> - </tableViewCell> - </objects> -</document> diff --git a/Ring/Ring/Walkthrough/CreateProfileViewController.swift b/Ring/Ring/Walkthrough/CreateProfileViewController.swift deleted file mode 100644 index 83e1a2bd2774533723e79843e10b29c57155db42..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/CreateProfileViewController.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2016 Savoir-faire Linux Inc. - * - * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -import UIKit - -enum ProfileCreationType { - case linkDeviceToAccount - case createProfile -} - -class CreateProfileViewController: UIViewController { - - var profileCreationType: ProfileCreationType? - - @IBAction func skip(_ sender: Any) { - if profileCreationType == .linkDeviceToAccount { - performSegue(withIdentifier: "ProfileToLinkSegue", sender: sender) - } else if profileCreationType == .createProfile { - performSegue(withIdentifier: "ProfileToAccountSegue", sender: sender) - } - } -} diff --git a/Ring/Ring/Walkthrough/CreateRingAccountViewController.swift b/Ring/Ring/Walkthrough/CreateRingAccountViewController.swift deleted file mode 100644 index 4430f5d5e2d84afeb0aae406f9910d59fb0abbac..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/CreateRingAccountViewController.swift +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2016 Savoir-faire Linux Inc. - * - * Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -import UIKit -import RxCocoa -import RxSwift -import PKHUD -import SwiftyBeaver - -fileprivate enum CreateRingAccountCellType { - case registerPublicUsername - case usernameField - case passwordNotice - case newPasswordField - case repeatPasswordField -} - -class CreateRingAccountViewController: UITableViewController { - - /** - logguer - */ - private let log = SwiftyBeaver.self - - var accountViewModel = CreateRingAccountViewModel(withAccountService: AppDelegate.accountService, - nameService: AppDelegate.nameService) - - @IBOutlet weak var createAccountButton: DesignableButton! - @IBOutlet weak var createAccountTitleLabel: UILabel! - - var disposeBag = DisposeBag() - - override func viewDidLoad() { - super.viewDidLoad() - - self.registerCells() - - self.bindViews() - - self.setupUI() - } - - func registerCells() { - self.tableView.register(cellType: SwitchCell.self) - self.tableView.register(cellType: TextFieldCell.self) - self.tableView.register(cellType: TextCell.self) - } - - /** - Bind all the necessary of this View to its ViewModel. - That allows to build the binding part of the MVVM pattern. - */ - fileprivate func bindViews() { - - //Add Account button action - self.createAccountButton - .rx - .tap - .takeUntil(self.rx.deallocated) - .subscribe(onNext: { - self.accountViewModel.createAccount() - }) - .disposed(by: self.disposeBag) - - //Add Account Registration state - self.accountViewModel.accountCreationState.observeOn(MainScheduler.instance).subscribe( - onNext: { [unowned self] state in - switch state { - case .started: - self.setCreateAccountAsLoading() - case .success: - self.setCreateAccountAsIdle() - self.showDeviceAddedAlert() - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let vc = storyboard.instantiateViewController(withIdentifier: "MainStoryboard") as UIViewController - self.dismiss(animated: true, completion: nil) - self.present(vc, animated: true, completion: nil) - default: - return - } - }, - onError: { [unowned self] error in - if let error = error as? AccountCreationError { - self.showErrorAlert(error) - } - self.setCreateAccountAsIdle() - }).disposed(by: disposeBag) - - //Show or hide user name field - self.accountViewModel.registerUsername.asObservable() - .subscribe(onNext: { [weak self] showUsernameField in - self?.toggleRegisterSwitch(showUsernameField) - }).disposed(by: disposeBag) - - //Enables create account button - self.accountViewModel.canCreateAccount - .bind(to: self.createAccountButton.rx.isEnabled) - .disposed(by: disposeBag) - } - - /** - Customize the views - */ - - fileprivate func setupUI() { - self.tableView.estimatedRowHeight = 44.0 - self.tableView.rowHeight = UITableViewAutomaticDimension - self.createAccountTitleLabel.text = L10n.Createaccount.createAccountFormTitle - } - - fileprivate func setCreateAccountAsLoading() { - log.debug("Creating account...") - self.createAccountButton.setTitle(L10n.Createaccount.loading, for: .normal) - self.createAccountButton.isUserInteractionEnabled = false - HUD.show(.labeledProgress(title: L10n.Createaccount.waitCreateAccountTitle, subtitle: nil)) - } - - fileprivate func setCreateAccountAsIdle() { - self.createAccountButton.setTitle(L10n.Welcome.createAccount, for: .normal) - self.createAccountButton.isUserInteractionEnabled = true - HUD.hide() - } - - fileprivate func showDeviceAddedAlert() { - HUD.flash(.labeledSuccess(title: L10n.Alerts.accountAddedTitle, subtitle: nil), delay: Durations.alertFlashDuration.value) - } - - fileprivate func showErrorAlert(_ error: AccountCreationError) { - let alert = UIAlertController.init(title: error.title, - message: error.message, - preferredStyle: .alert) - alert.addAction(UIAlertAction.init(title: L10n.Global.ok, style: .default, handler: nil)) - self.present(alert, animated: true, completion: nil) - } - - /** - Show or hide the username field cell in function of the switch state - */ - - func toggleRegisterSwitch(_ show: Bool) { - - let usernameFieldCellIndex = 1 - - if show && !cells.contains(.usernameField) { - self.cells.insert(.usernameField, at: usernameFieldCellIndex) - self.tableView.insertRows(at: [IndexPath(row: usernameFieldCellIndex, section: 0)], - with: .automatic) - } else if !show && cells.contains(.usernameField) { - self.cells.remove(at: usernameFieldCellIndex) - self.tableView.deleteRows(at: [IndexPath(row: usernameFieldCellIndex, section: 0)], - with: .automatic) - } - - } - - // MARK: TableView datasource - fileprivate var cells: [CreateRingAccountCellType] = [.registerPublicUsername, - .passwordNotice, - .newPasswordField, - .repeatPasswordField] - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return cells.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - let currentCellType = cells[indexPath.row] - - if currentCellType == .registerPublicUsername { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SwitchCell.self) - - cell.titleLabel.text = L10n.Createaccount.registerPublicUsername - cell.titleLabel.textColor = .white - cell.registerSwitch.rx.value.bind(to: self.accountViewModel.registerUsername).disposed(by: disposeBag) - return cell - } else if currentCellType == .usernameField { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: TextFieldCell.self) - - cell.textField.isSecureTextEntry = false - cell.textField.placeholder = L10n.Createaccount.enterNewUsernamePlaceholder - - //Binds the username field value to the ViewModel - cell.textField.rx.text.orEmpty - .throttle(Durations.textFieldThrottlingDuration.value, scheduler: MainScheduler.instance) - .distinctUntilChanged() - .bind(to: self.accountViewModel.username) - .disposed(by: disposeBag) - - //Switch to new password cell when return button is touched - cell.textField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { - self.switchToCell(withType: .newPasswordField) - }).disposed(by: disposeBag) - - self.accountViewModel.usernameValidationMessage.bind(to: cell.errorMessageLabel.rx.text).disposed(by: disposeBag) - return cell - } else if currentCellType == .passwordNotice { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: TextCell.self) - cell.label.text = L10n.Createaccount.chooseStrongPassword - return cell - } else if currentCellType == .newPasswordField { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: TextFieldCell.self) - - cell.textField.isSecureTextEntry = true - cell.textField.placeholder = L10n.Createaccount.newPasswordPlaceholder - cell.errorMessageLabel.text = L10n.Createaccount.passwordCharactersNumberError - - //Binds the password field value to the ViewModel - cell.textField.rx.text.orEmpty.bind(to: self.accountViewModel.password).disposed(by: disposeBag) - - //Binds the observer to show the error label if the field is not empty - self.accountViewModel.hidePasswordError.bind(to: cell.errorMessageLabel.rx.isHidden).disposed(by: disposeBag) - - //Switch to the repeat pasword cell when return button is touched - cell.textField.rx.controlEvent(.editingDidEndOnExit) - .subscribe(onNext: { - self.switchToCell(withType: .repeatPasswordField) - }).disposed(by: disposeBag) - - return cell - } else { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: TextFieldCell.self) - - cell.textField.isSecureTextEntry = true - cell.textField.placeholder = L10n.Createaccount.repeatPasswordPlaceholder - cell.errorMessageLabel.text = L10n.Createaccount.passwordNotMatchingError - - //Binds the repeat password field value to the ViewModel - cell.textField.rx.text.orEmpty.bind(to: self.accountViewModel.repeatPassword).disposed(by: disposeBag) - - //Binds the observer to the text field 'hidden' property - self.accountViewModel.hideRepeatPasswordError.bind(to: cell.errorMessageLabel.rx.isHidden).disposed(by: disposeBag) - - return cell - - } - } - - fileprivate func switchToCell(withType cellType: CreateRingAccountCellType) { - if let cellIndex = self.cells.index(of: cellType) { - if let cell = tableView.cellForRow(at: IndexPath(row: cellIndex, section: 0)) - as? TextFieldCell { - cell.textField.becomeFirstResponder() - } - self.tableView.scrollToRow(at: IndexPath(row: cellIndex, section: 0), - at: .bottom, animated: false) - } - } -} diff --git a/Ring/Ring/Walkthrough/WalkthroughStoryboard.storyboard b/Ring/Ring/Walkthrough/WalkthroughStoryboard.storyboard deleted file mode 100644 index b56028fd294fbd011458471f2c1156b7b7f3d79d..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/WalkthroughStoryboard.storyboard +++ /dev/null @@ -1,332 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="GnB-zf-djy"> - <device id="retina3_5" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <scenes> - <!--Navigation Controller--> - <scene sceneID="azk-5X-z35"> - <objects> - <navigationController navigationBarHidden="YES" id="GnB-zf-djy" sceneMemberID="viewController"> - <navigationBar key="navigationBar" contentMode="scaleToFill" id="Pgv-sV-7YL"> - <rect key="frame" x="0.0" y="0.0" width="375" height="44"/> - <autoresizingMask key="autoresizingMask"/> - </navigationBar> - <toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="EHH-WC-Pef"> - <rect key="frame" x="0.0" y="623" width="375" height="44"/> - <autoresizingMask key="autoresizingMask"/> - </toolbar> - <connections> - <segue destination="zOM-us-BHp" kind="relationship" relationship="rootViewController" id="Cs0-7s-oVR"/> - </connections> - </navigationController> - <placeholder placeholderIdentifier="IBFirstResponder" id="c5B-LX-9rY" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-1016" y="365"/> - </scene> - <!--Welcome View Controller--> - <scene sceneID="BPo-UM-NNL"> - <objects> - <viewController id="zOM-us-BHp" customClass="WelcomeViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="Lps-eE-m7k"/> - <viewControllerLayoutGuide type="bottom" id="OgW-31-HCB"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="GmX-eQ-kCs"> - <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="DXu-A1-gcl"> - <rect key="frame" x="25" y="90" width="271" height="300"/> - <subviews> - <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" verticalHuggingPriority="251" image="logo-ring-beta2-blanc" translatesAutoresizingMaskIntoConstraints="NO" id="7CK-fT-m09"> - <rect key="frame" x="45.5" y="0.0" width="180" height="66"/> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstAttribute="width" constant="180" id="f7g-Q8-q8s"/> - <constraint firstAttribute="height" constant="66" id="sjl-FP-LWX"/> - </constraints> - </imageView> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="750" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Welcome to Ring" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SnZ-RF-3dF"> - <rect key="frame" x="45.5" y="82" width="180.5" height="29"/> - <fontDescription key="fontDescription" type="system" pointSize="24"/> - <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/> - <nil key="highlightedColor"/> - </label> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="A Ring account allows you to reach people securely in peer to peer through fully distributed network" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PxM-F8-mIM"> - <rect key="frame" x="0.0" y="127" width="271" height="61"/> - <fontDescription key="fontDescription" type="system" pointSize="17"/> - <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/> - <nil key="highlightedColor"/> - </label> - <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="R0q-9u-3WR" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="20" y="204" width="231" height="40"/> - <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstAttribute="height" constant="40" id="d0X-Cl-Ry4"/> - </constraints> - <state key="normal" title="Link this device to an account"> - <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </state> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> - </userDefinedRuntimeAttributes> - <connections> - <action selector="linkDeviceToAccountAction:" destination="zOM-us-BHp" eventType="touchUpInside" id="bul-x6-xBK"/> - </connections> - </button> - <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8Ve-ZD-TXf" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="20" y="260" width="231" height="40"/> - <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstAttribute="height" constant="40" id="5Qb-T3-Su0"/> - </constraints> - <state key="normal" title="Create a Ring account"> - <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </state> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> - </userDefinedRuntimeAttributes> - <connections> - <action selector="createAccountAction:" destination="zOM-us-BHp" eventType="touchUpInside" id="o0k-MC-tsv"/> - </connections> - </button> - </subviews> - <constraints> - <constraint firstItem="8Ve-ZD-TXf" firstAttribute="leading" secondItem="R0q-9u-3WR" secondAttribute="leading" id="U0b-w9-yjf"/> - <constraint firstAttribute="trailing" secondItem="R0q-9u-3WR" secondAttribute="trailing" constant="20" id="WpG-GI-pPu"/> - <constraint firstItem="8Ve-ZD-TXf" firstAttribute="trailing" secondItem="R0q-9u-3WR" secondAttribute="trailing" id="bE8-0O-Ead"/> - <constraint firstItem="R0q-9u-3WR" firstAttribute="leading" secondItem="DXu-A1-gcl" secondAttribute="leading" constant="20" id="yWE-sO-6tc"/> - </constraints> - </stackView> - </subviews> - <color key="backgroundColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstItem="DXu-A1-gcl" firstAttribute="centerY" secondItem="GmX-eQ-kCs" secondAttribute="centerY" id="65t-Vk-4ms"/> - <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="DXu-A1-gcl" secondAttribute="trailing" constant="16" id="Ief-ih-29J"/> - <constraint firstItem="OgW-31-HCB" firstAttribute="top" relation="greaterThanOrEqual" secondItem="DXu-A1-gcl" secondAttribute="bottom" constant="16" id="NQ7-yl-xKL"/> - <constraint firstItem="DXu-A1-gcl" firstAttribute="centerX" secondItem="GmX-eQ-kCs" secondAttribute="centerX" id="RZK-gk-Fpg"/> - <constraint firstItem="DXu-A1-gcl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="GmX-eQ-kCs" secondAttribute="leading" constant="16" id="XDW-06-xaK"/> - <constraint firstItem="DXu-A1-gcl" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Lps-eE-m7k" secondAttribute="bottom" constant="16" id="fqS-K5-fFB"/> - </constraints> - </view> - <navigationItem key="navigationItem" id="LPh-IM-uvN"/> - <connections> - <outlet property="createAccountButton" destination="8Ve-ZD-TXf" id="OBF-nO-YAa"/> - <outlet property="descriptionLabel" destination="PxM-F8-mIM" id="Bbw-uI-ltO"/> - <outlet property="linkDeviceButton" destination="R0q-9u-3WR" id="W2P-ar-nh0"/> - <outlet property="welcomeLabel" destination="SnZ-RF-3dF" id="Rgf-TM-GFn"/> - <segue destination="dmb-i6-bo9" kind="show" identifier="CreateProfileSegue" id="GoD-mz-wCr"/> - <segue destination="dmb-i6-bo9" kind="show" identifier="LinkDeviceToAccountSegue" id="k4b-a3-FoK"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="PWI-c5-979" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="-66" y="366"/> - </scene> - <!--Create Profile View Controller--> - <scene sceneID="NEP-n0-rvD"> - <objects> - <viewController id="dmb-i6-bo9" customClass="CreateProfileViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="tU2-L3-HzU"/> - <viewControllerLayoutGuide type="bottom" id="Zbo-H6-yOO"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="FYO-p5-MWn"> - <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8M5-AJ-pxH" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="110" y="220" width="100" height="40"/> - <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstAttribute="width" constant="100" id="2t1-a8-dxY"/> - <constraint firstAttribute="height" constant="40" id="cln-HK-ZE8"/> - </constraints> - <state key="normal" title="Skip"> - <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </state> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> - </userDefinedRuntimeAttributes> - <connections> - <action selector="skip:" destination="dmb-i6-bo9" eventType="touchUpInside" id="PAR-5w-VRm"/> - </connections> - </button> - </subviews> - <color key="backgroundColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstItem="8M5-AJ-pxH" firstAttribute="centerX" secondItem="FYO-p5-MWn" secondAttribute="centerX" id="kWB-xC-CED"/> - <constraint firstItem="8M5-AJ-pxH" firstAttribute="centerY" secondItem="FYO-p5-MWn" secondAttribute="centerY" id="nsJ-Gi-lEl"/> - </constraints> - </view> - <connections> - <segue destination="kzh-87-Ao9" kind="show" identifier="ProfileToAccountSegue" id="CDO-bH-qiw"/> - <segue destination="bDM-Sb-hbx" kind="show" identifier="ProfileToLinkSegue" id="dop-gF-ixP"/> - </connections> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="YDh-Qj-wfv" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="1105" y="367"/> - </scene> - <!--Table View Controller--> - <scene sceneID="2lN-mL-RUf"> - <objects> - <tableViewController id="bDM-Sb-hbx" sceneMemberID="viewController"> - <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="eFu-ir-jgw"> - <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <color key="backgroundColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - <view key="tableHeaderView" contentMode="scaleToFill" id="3uF-hj-J2O" userLabel="Header"> - <rect key="frame" x="0.0" y="0.0" width="320" height="130"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Link this device" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hEw-Wn-alu"> - <rect key="frame" x="8" y="8" width="304" height="114"/> - <fontDescription key="fontDescription" type="system" pointSize="24"/> - <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="hEw-Wn-alu" firstAttribute="leading" secondItem="3uF-hj-J2O" secondAttribute="leading" constant="8" id="00U-c3-n95"/> - <constraint firstAttribute="trailing" secondItem="hEw-Wn-alu" secondAttribute="trailing" constant="8" id="Aut-xd-LuF"/> - <constraint firstAttribute="bottom" secondItem="hEw-Wn-alu" secondAttribute="bottom" constant="8" id="TDc-e8-wb3"/> - <constraint firstItem="hEw-Wn-alu" firstAttribute="top" secondItem="3uF-hj-J2O" secondAttribute="top" constant="8" id="auu-2K-J6Y"/> - </constraints> - </view> - <view key="tableFooterView" contentMode="scaleToFill" id="bQ2-6Y-CIW" userLabel="Footer"> - <rect key="frame" x="0.0" y="130" width="320" height="54"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <subviews> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UQJ-hF-j2O" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="110" y="7" width="100" height="40"/> - <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstAttribute="height" constant="40" id="RGF-Ib-dT9"/> - <constraint firstAttribute="width" constant="100" id="p9O-j8-fcB"/> - </constraints> - <state key="normal" title="Link Device"> - <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </state> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> - </userDefinedRuntimeAttributes> - </button> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="UQJ-hF-j2O" firstAttribute="centerX" secondItem="bQ2-6Y-CIW" secondAttribute="centerX" id="7LD-pW-9BH"/> - <constraint firstItem="UQJ-hF-j2O" firstAttribute="centerY" secondItem="bQ2-6Y-CIW" secondAttribute="centerY" id="f6F-dX-f0m"/> - </constraints> - </view> - <connections> - <outlet property="dataSource" destination="bDM-Sb-hbx" id="4fq-3g-DLG"/> - <outlet property="delegate" destination="bDM-Sb-hbx" id="caJ-9P-wkv"/> - </connections> - </tableView> - </tableViewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="Xl1-4e-7qX" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="2078" y="-216"/> - </scene> - <!--Create Ring Account View Controller--> - <scene sceneID="XTt-go-ZNJ"> - <objects> - <tableViewController id="kzh-87-Ao9" customClass="CreateRingAccountViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> - <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="69" sectionHeaderHeight="28" sectionFooterHeight="28" id="e9y-lu-Idq"> - <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <color key="backgroundColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> - <view key="tableHeaderView" contentMode="scaleToFill" id="gnd-b0-Ncm" userLabel="Header"> - <rect key="frame" x="0.0" y="0.0" width="320" height="130"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Create your Ring account" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AIX-eC-baN"> - <rect key="frame" x="8" y="8" width="304" height="114"/> - <fontDescription key="fontDescription" type="system" pointSize="24"/> - <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstItem="AIX-eC-baN" firstAttribute="leading" secondItem="gnd-b0-Ncm" secondAttribute="leading" constant="8" id="D6R-DY-N4I"/> - <constraint firstAttribute="trailing" secondItem="AIX-eC-baN" secondAttribute="trailing" constant="8" id="c58-zW-D8i"/> - <constraint firstAttribute="bottom" secondItem="AIX-eC-baN" secondAttribute="bottom" constant="8" id="k5Z-6d-hx8"/> - <constraint firstItem="AIX-eC-baN" firstAttribute="top" secondItem="gnd-b0-Ncm" secondAttribute="top" constant="8" id="peB-GQ-GOz"/> - </constraints> - </view> - <view key="tableFooterView" contentMode="scaleToFill" id="MnN-De-arJ" userLabel="Footer"> - <rect key="frame" x="0.0" y="130" width="320" height="54"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> - <subviews> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gsT-SB-0AP" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> - <rect key="frame" x="70" y="7" width="180" height="40"/> - <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="calibratedRGB"/> - <constraints> - <constraint firstAttribute="height" constant="40" id="b0c-dD-zDx"/> - </constraints> - <state key="normal" title="Create a Ring account"> - <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </state> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> - </userDefinedRuntimeAttributes> - </button> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> - <constraints> - <constraint firstAttribute="trailing" secondItem="gsT-SB-0AP" secondAttribute="trailing" constant="70" id="MsB-8o-KHc"/> - <constraint firstItem="gsT-SB-0AP" firstAttribute="leading" secondItem="MnN-De-arJ" secondAttribute="leading" constant="70" id="PjH-ny-nr4"/> - <constraint firstItem="gsT-SB-0AP" firstAttribute="centerY" secondItem="MnN-De-arJ" secondAttribute="centerY" id="gt1-U3-e6s"/> - <constraint firstItem="gsT-SB-0AP" firstAttribute="centerX" secondItem="MnN-De-arJ" secondAttribute="centerX" id="tMA-Qg-oxa"/> - </constraints> - </view> - <connections> - <outlet property="dataSource" destination="kzh-87-Ao9" id="fWb-k5-HBL"/> - <outlet property="delegate" destination="kzh-87-Ao9" id="DE2-dv-wl2"/> - </connections> - </tableView> - <connections> - <outlet property="createAccountButton" destination="gsT-SB-0AP" id="SxP-Y3-eoo"/> - <outlet property="createAccountTitleLabel" destination="AIX-eC-baN" id="kg4-gi-v7Q"/> - <segue destination="E3W-r7-J4y" kind="show" identifier="AccountToPermissionsSegue" id="Zig-iC-h9U"/> - </connections> - </tableViewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="W0G-TV-Z9c" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="2077.5" y="580"/> - </scene> - <!--View Controller--> - <scene sceneID="c8H-6M-3dO"> - <objects> - <viewController id="E3W-r7-J4y" sceneMemberID="viewController"> - <layoutGuides> - <viewControllerLayoutGuide type="top" id="fe9-Ak-7RC"/> - <viewControllerLayoutGuide type="bottom" id="9DO-dt-Omo"/> - </layoutGuides> - <view key="view" contentMode="scaleToFill" id="DGL-2A-dSe"> - <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> - </view> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="bS8-E0-lOu" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="3049" y="581"/> - </scene> - </scenes> - <resources> - <image name="logo-ring-beta2-blanc" width="180" height="66"/> - </resources> - <inferredMetricsTieBreakers> - <segue reference="k4b-a3-FoK"/> - </inferredMetricsTieBreakers> -</document> diff --git a/Ring/Ring/Walkthrough/WelcomeViewController.swift b/Ring/Ring/Walkthrough/WelcomeViewController.swift deleted file mode 100644 index 0b2f408c37d0c356a0918d68e2991a2e5341b734..0000000000000000000000000000000000000000 --- a/Ring/Ring/Walkthrough/WelcomeViewController.swift +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2017 Savoir-faire Linux Inc. - * - * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -import UIKit - -class WelcomeViewController: UIViewController { - - @IBOutlet weak var welcomeLabel: UILabel! - @IBOutlet weak var descriptionLabel: UILabel! - @IBOutlet weak var linkDeviceButton: DesignableButton! - @IBOutlet weak var createAccountButton: DesignableButton! - - let createProfileSegueIdentifier = "CreateProfileSegue" - let linkDeviceToAccountSegueIdentifier = "LinkDeviceToAccountSegue" - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: true) - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - @IBAction func linkDeviceToAccountAction(_ sender: Any) { - self.performSegue(withIdentifier: linkDeviceToAccountSegueIdentifier, sender: nil) - } - - @IBAction func createAccountAction(_ sender: Any) { - self.performSegue(withIdentifier: createProfileSegueIdentifier, sender: nil) - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - self.navigationController?.setNavigationBarHidden(false, animated: true) - - var profileCreationType: ProfileCreationType? - - if segue.identifier == createProfileSegueIdentifier { - profileCreationType = .createProfile - } else if segue.identifier == linkDeviceToAccountSegueIdentifier { - profileCreationType = .linkDeviceToAccount - } - - if let createProfileViewController = segue.destination as? CreateProfileViewController { - createProfileViewController.profileCreationType = profileCreationType - } - } - - func setupUI() { - - self.welcomeLabel.text = L10n.Welcome.title - self.descriptionLabel.text = L10n.Welcome.text - self.linkDeviceButton.setTitle(L10n.Welcome.linkDeviceButton, for: .normal) - self.createAccountButton.setTitle(L10n.Welcome.createAccount, for: .normal) - } -}