diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 642cad9dacad04100ce2066234d7288cf7d7426a..e713d124419ace96a0a75762888f8783c122d779 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -221,7 +221,7 @@ 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 */; }; + 1A2D18FD1F292DAD00B2C785 /* SmartListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1A2D18FB1F292DAD00B2C785 /* SmartListCell.xib */; }; 1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D18FE1F29352D00B2C785 /* MeViewModel.swift */; }; 1A2D19011F29353A00B2C785 /* MeDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2D19001F29353A00B2C785 /* MeDetailViewModel.swift */; }; 1A3D28A71F0EB9DB00B524EE /* Bool+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3D28A61F0EB9DB00B524EE /* Bool+String.swift */; }; @@ -247,6 +247,15 @@ 1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DA1F0D915100D36361 /* Localizable.strings */; }; 1ABE07DF1F0D91A800D36361 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */; }; 1ABE07E21F0D924700D36361 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABE07E11F0D924700D36361 /* Strings.swift */; }; + 263B7158246D9390007044C4 /* SmartListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263B7157246D9390007044C4 /* SmartListCell.swift */; }; + 263B715A246D9556007044C4 /* IncognitoSmartListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263B7159246D9556007044C4 /* IncognitoSmartListCell.swift */; }; + 263B715C246D96E5007044C4 /* IncognitoSmartListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 263B715B246D96E5007044C4 /* IncognitoSmartListCell.xib */; }; + 2662FC79246B1E1700FA7782 /* JamiSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC78246B1E1700FA7782 /* JamiSearchView.swift */; }; + 2662FC7B246B216B00FA7782 /* JamiSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC7A246B216B00FA7782 /* JamiSearchViewModel.swift */; }; + 2662FC7D246B78E800FA7782 /* IncognitoSmartListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */; }; + 2662FC7F246B790400FA7782 /* IncognitoSmartListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2662FC7E246B790400FA7782 /* IncognitoSmartListViewModel.swift */; }; + 2662FC81246B793500FA7782 /* IncognitoSmartListViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */; }; + 268AA5C12472D42700B654A0 /* ConfirmationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268AA5C02472D42700B654A0 /* ConfirmationAlert.swift */; }; 4430A66B236CBA7D00747177 /* ContactPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4430A66A236CBA7D00747177 /* ContactPickerSection.swift */; }; 4430A66D236CBC5900747177 /* ContactPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4430A66C236CBC5900747177 /* ContactPickerViewController.storyboard */; }; 4430A66F236CBC6900747177 /* ContactPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4430A66E236CBC6900747177 /* ContactPickerViewController.swift */; }; @@ -562,7 +571,7 @@ 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>"; }; + 1A2D18FB1F292DAD00B2C785 /* SmartListCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SmartListCell.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>"; }; 1A3CA32A1F102BB700283748 /* Chameleon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Chameleon.framework; path = Carthage/Build/iOS/Chameleon.framework; sourceTree = "<group>"; }; @@ -592,6 +601,15 @@ 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Resources/LaunchScreen.storyboard; sourceTree = "<group>"; }; 1ABE07E11F0D924700D36361 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; }; 26376721245315E600CDC51F /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = "<group>"; }; + 263B7157246D9390007044C4 /* SmartListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartListCell.swift; sourceTree = "<group>"; }; + 263B7159246D9556007044C4 /* IncognitoSmartListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoSmartListCell.swift; sourceTree = "<group>"; }; + 263B715B246D96E5007044C4 /* IncognitoSmartListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IncognitoSmartListCell.xib; sourceTree = "<group>"; }; + 2662FC78246B1E1700FA7782 /* JamiSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamiSearchView.swift; sourceTree = "<group>"; }; + 2662FC7A246B216B00FA7782 /* JamiSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamiSearchViewModel.swift; sourceTree = "<group>"; }; + 2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IncognitoSmartListViewController.swift; path = Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift; sourceTree = SOURCE_ROOT; }; + 2662FC7E246B790400FA7782 /* IncognitoSmartListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IncognitoSmartListViewModel.swift; path = Ring/Features/Conversations/SmartList/IncognitoSmartListViewModel.swift; sourceTree = SOURCE_ROOT; }; + 2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = IncognitoSmartListViewController.storyboard; path = Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.storyboard; sourceTree = SOURCE_ROOT; }; + 268AA5C02472D42700B654A0 /* ConfirmationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationAlert.swift; sourceTree = "<group>"; }; 4430A66A236CBA7D00747177 /* ContactPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerSection.swift; sourceTree = "<group>"; }; 4430A66C236CBC5900747177 /* ContactPickerViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ContactPickerViewController.storyboard; sourceTree = "<group>"; }; 4430A66E236CBC6900747177 /* ContactPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerViewController.swift; sourceTree = "<group>"; }; @@ -1194,9 +1212,11 @@ 0E5806F123BE42C8007D1F5D /* views */ = { isa = PBXGroup; children = ( + 2662FC77246B1DD600FA7782 /* JamiSearchView */, 0E5806F223BE42EC007D1F5D /* PlayerView.swift */, 0E5806F423BE4307007D1F5D /* PlayerViewModel.swift */, 0E5806F623BE4325007D1F5D /* PlayerView.xib */, + 268AA5C02472D42700B654A0 /* ConfirmationAlert.swift */, ); path = views; sourceTree = "<group>"; @@ -1427,6 +1447,9 @@ 1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */, 1A2D18B51F29164700B2C785 /* SmartlistViewModel.swift */, 1A5DC0311F3566140075E8EF /* ConversationSection.swift */, + 2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */, + 2662FC7E246B790400FA7782 /* IncognitoSmartListViewModel.swift */, + 2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */, ); path = Smartlist; sourceTree = "<group>"; @@ -1510,9 +1533,12 @@ children = ( 0E6F544E223C0ED600ECC3CE /* AccountPickerAdapter.swift */, 1A2D18FA1F292DAD00B2C785 /* ConversationCell.swift */, - 1A2D18FB1F292DAD00B2C785 /* ConversationCell.xib */, + 1A2D18FB1F292DAD00B2C785 /* SmartListCell.xib */, 0E6F5450223C3C4F00ECC3CE /* AccountItemView.xib */, 0E6F5452223C3C7500ECC3CE /* AccountItemView.swift */, + 263B7157246D9390007044C4 /* SmartListCell.swift */, + 263B7159246D9556007044C4 /* IncognitoSmartListCell.swift */, + 263B715B246D96E5007044C4 /* IncognitoSmartListCell.xib */, ); name = Cells; path = ../SmartList/Cells; @@ -1561,6 +1587,15 @@ path = Resources; sourceTree = "<group>"; }; + 2662FC77246B1DD600FA7782 /* JamiSearchView */ = { + isa = PBXGroup; + children = ( + 2662FC78246B1E1700FA7782 /* JamiSearchView.swift */, + 2662FC7A246B216B00FA7782 /* JamiSearchViewModel.swift */, + ); + path = JamiSearchView; + sourceTree = "<group>"; + }; 4430A669236CB9F600747177 /* Conference */ = { isa = PBXGroup; children = ( @@ -1833,7 +1868,7 @@ buildActionMask = 2147483647; files = ( 0E6F545622403ADE00ECC3CE /* CreateSipAccountViewController.storyboard in Resources */, - 1A2D18FD1F292DAD00B2C785 /* ConversationCell.xib in Resources */, + 1A2D18FD1F292DAD00B2C785 /* SmartListCell.xib in Resources */, 0E320D50224ADF840070B515 /* DialpadViewController.storyboard in Resources */, 1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */, 1A2D18E61F29197100B2C785 /* MessageAccessoryView.xib in Resources */, @@ -1843,6 +1878,7 @@ 0ED2B6FA1F96A075001572F0 /* LinkNewDeviceViewController.storyboard in Resources */, 1A2D18EF1F291A0100B2C785 /* MeDetailViewController.storyboard in Resources */, 623660AA20092081002598C1 /* src in Resources */, + 263B715C246D96E5007044C4 /* IncognitoSmartListCell.xib in Resources */, 1A2D18B11F2915B600B2C785 /* SmartlistViewController.storyboard in Resources */, 0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */, 0E6F5451223C3C4F00ECC3CE /* AccountItemView.xib in Resources */, @@ -1858,6 +1894,7 @@ 6613A612214AFF4700B497D1 /* ScanViewController.storyboard in Resources */, 1A20417E1F1E8DDA00C08435 /* CreateProfileViewController.storyboard in Resources */, 1ABE07DF1F0D91A800D36361 /* LaunchScreen.storyboard in Resources */, + 2662FC81246B793500FA7782 /* IncognitoSmartListViewController.storyboard in Resources */, 1A5DC0381F35675E0075E8EF /* ContactRequestCell.xib in Resources */, 0ECA56812433948E0055D31E /* MigrateAccountViewController.storyboard in Resources */, 1A0C4EDA1F1D4B1B00550433 /* WelcomeViewController.storyboard in Resources */, @@ -1987,6 +2024,7 @@ buildActionMask = 2147483647; files = ( 557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */, + 263B7158246D9390007044C4 /* SmartListCell.swift in Sources */, 0586C94B1F684DF600613517 /* UIImage+Helpers.swift in Sources */, 4430A671236CBC7A00747177 /* ContactPickerViewModel.swift in Sources */, 621231F91F880EDF009B86F0 /* UILabel+Ring.swift in Sources */, @@ -2056,6 +2094,7 @@ 0E0FF1B91FC398C5003898C2 /* InteractionDataHelper.swift in Sources */, 0E6F545822403B0300ECC3CE /* CreateSipAccountViewController.swift in Sources */, 0EE1B54E1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift in Sources */, + 268AA5C12472D42700B654A0 /* ConfirmationAlert.swift in Sources */, 0E0FF1AA1FC3843E003898C2 /* DBContainer.swift in Sources */, 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */, 0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */, @@ -2063,8 +2102,10 @@ 0E6F544D223BFE3E00ECC3CE /* DisposableCell.swift in Sources */, 0E9D84491FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift in Sources */, 621231FB1F8D6FEE009B86F0 /* MessageCell.swift in Sources */, + 2662FC7D246B78E800FA7782 /* IncognitoSmartListViewController.swift in Sources */, 56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */, 1A2D189A1F2642C000B2C785 /* NotificationCenter+Ring.swift in Sources */, + 2662FC79246B1E1700FA7782 /* JamiSearchView.swift in Sources */, 0E44B62F202B9DE40060F71B /* LocalNotificationsHelper.swift in Sources */, 0E6F545A22403B1D00ECC3CE /* CreateSipAccountViewModel.swift in Sources */, 1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */, @@ -2098,6 +2139,8 @@ 62AA15C31FFC39C80064A063 /* VideoAdapterDelegate.swift in Sources */, 04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */, 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */, + 2662FC7B246B216B00FA7782 /* JamiSearchViewModel.swift in Sources */, + 2662FC7F246B790400FA7782 /* IncognitoSmartListViewModel.swift in Sources */, 446FAF1D2373427100519C4F /* SendFileViewModel.swift in Sources */, 62AD584A2056DADF00AF0701 /* MessageCellDataTransferReceived.swift in Sources */, 0E0FF1AF1FC38CBC003898C2 /* ProfileDataHelper.swift in Sources */, @@ -2133,6 +2176,7 @@ 564C44621E943DE6000F92B1 /* NameService.swift in Sources */, 1A5DC02C1F3565250075E8EF /* MeViewController.swift in Sources */, 0EF78DE31FD0AE3000FC6966 /* ConversationsManager.swift in Sources */, + 263B715A246D9556007044C4 /* IncognitoSmartListCell.swift in Sources */, 66266FC021557D2F002757A6 /* ScanViewModel.swift in Sources */, 0E5A668322F0B1F100AA6820 /* ProgressView.swift in Sources */, 0E96ED79225D06480016C07D /* GeneralSettingsViewModel.swift in Sources */, diff --git a/Ring/Ring/Account/AccountModelHelper.swift b/Ring/Ring/Account/AccountModelHelper.swift index 20b9af6cb22affac3136756db3445aa10a301428..8e5c153e915253849bc300b696820210ccda49d1 100644 --- a/Ring/Ring/Account/AccountModelHelper.swift +++ b/Ring/Ring/Account/AccountModelHelper.swift @@ -181,7 +181,7 @@ struct AccountModelHelper { } } - public var havePassword: Bool { + public var hasPassword: Bool { let noPassword: String = self.account.details?.get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.archiveHasPassword)) ?? "false" return noPassword == "true" ? true : false } diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift index 79bd69153dfba4a0c09a128e6def5def07def8ab..f30469ca7007b311f7f1a95a01074e4201a04938 100644 --- a/Ring/Ring/AppDelegate.swift +++ b/Ring/Ring/AppDelegate.swift @@ -211,7 +211,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func reloadDataFor(account: AccountModel) { self.contactsService.loadContacts(withAccount: account) - self.contactsService.loadContactRequests(withAccount: account) + self.contactsService.loadContactRequests(withAccount: account.id) self.presenceService.subscribeBuddies(withAccount: account.id, withContacts: self.contactsService.contacts.value, subscribe: true) self.conversationManager? .prepareConversationsForAccount(accountId: account.id) @@ -424,6 +424,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if self.accountService.boothMode() { + return false + } guard let handle = userActivity.startCallHandle else { return false } diff --git a/Ring/Ring/Bridging/AccountCreation/AccountAdapter.h b/Ring/Ring/Bridging/AccountCreation/AccountAdapter.h index 94142dbf87f94609d1e0867b6ced835d43ac7a2b..56ceeca021b32561801ddc90c10a5e038502506f 100644 --- a/Ring/Ring/Bridging/AccountCreation/AccountAdapter.h +++ b/Ring/Ring/Bridging/AccountCreation/AccountAdapter.h @@ -77,5 +77,9 @@ - (void)pushNotificationReceived:(NSString *) from message:(NSDictionary*) data; - (void)setPushNotificationToken: (NSString *) token; +- (BOOL)passwordIsValid:(NSString *)accountId password:(NSString *)password; +- (BOOL)changeAccountPassword:(NSString *)accountId + oldPassword:(NSString *)oldpassword + newPassword:(NSString *)newPassword; @end diff --git a/Ring/Ring/Bridging/AccountCreation/AccountAdapter.mm b/Ring/Ring/Bridging/AccountCreation/AccountAdapter.mm index 1442db3e1185898e0e9d49dfee5af6c36ec31b0d..715cafda12aae180b18335cd7e0a21cc66fc6bb8 100644 --- a/Ring/Ring/Bridging/AccountCreation/AccountAdapter.mm +++ b/Ring/Ring/Bridging/AccountCreation/AccountAdapter.mm @@ -197,5 +197,16 @@ static id <AccountAdapterDelegate> _delegate; - (void)setPushNotificationToken: (NSString*)token { setPushNotificationToken(std::string([token UTF8String])); } +- (BOOL)passwordIsValid:(NSString *)accountId password:(NSString *)password { + return isPasswordValid(std::string([accountId UTF8String]), std::string([password UTF8String])); +} + +- (BOOL)changeAccountPassword:(NSString *)accountId + oldPassword:(NSString *)oldpassword + newPassword:(NSString *)newPassword { + return changeAccountPassword(std::string([accountId UTF8String]), + std::string([oldpassword UTF8String]), + std::string([newPassword UTF8String])); +} @end diff --git a/Ring/Ring/Calls/CallViewController.swift b/Ring/Ring/Calls/CallViewController.swift index cd0435fed5bee1f8b4e94d8bbc749c936dd06780..64b2dca1bfbb4f18b37c8e7574dc060c4f6f7486 100644 --- a/Ring/Ring/Calls/CallViewController.swift +++ b/Ring/Ring/Calls/CallViewController.swift @@ -137,6 +137,9 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { self.setAvatarView(!self.avatarView.isHidden) self.callPulse.layer.cornerRadius = (self.profileImageViewWidthConstraint.constant - 20) * 0.5 }).disposed(by: self.disposeBag) + sendMessageButton.isHidden = self.viewModel.isBoothMode() + sendMessageButton.isEnabled = !self.viewModel.isBoothMode() + buttonsStackView.isHidden = self.viewModel.isBoothMode() } @IBAction func addParticipant(_ sender: Any) { @@ -542,7 +545,7 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased { func removeFromScreen() { UIDevice.current.isProximityMonitoringEnabled = false UIApplication.shared.isIdleTimerDisabled = false - self.viewModel.showConversations() + self.viewModel.callFinished() self.dismiss(animated: false) } diff --git a/Ring/Ring/Calls/CallViewModel.swift b/Ring/Ring/Calls/CallViewModel.swift index 4d5601861833e3174850fbc009c82e9b8e3ed73a..ff31a28e76db9df98e566842ef6cc14dd8341847 100644 --- a/Ring/Ring/Calls/CallViewModel.swift +++ b/Ring/Ring/Calls/CallViewModel.swift @@ -459,6 +459,9 @@ class CallViewModel: Stateable, ViewModel { .subscribe(onSuccess: { [weak self] callModel in callModel.callUUID = UUID() self?.call = callModel + if self?.isBoothMode() ?? false { + return + } self?.callsProvider .startCall(account: account, call: callModel) }).disposed(by: self.disposeBag) @@ -543,4 +546,19 @@ class CallViewModel: Stateable, ViewModel { conversationViewModel.conversation = Variable<ConversationModel>(conversation) self.stateSubject.onNext(ConversationState.fromCallToConversation(conversation: conversationViewModel)) } + + func isBoothMode() -> Bool { + return self.accountService.boothMode() + } + + func callFinished() { + guard let accountId = self.call?.accountId else { + return + } + if self.isBoothMode() { + self.contactsService.removeAllContacts(for: accountId) + return + } + self.showConversations() + } } diff --git a/Ring/Ring/Calls/Conference/ContactPickerViewController.swift b/Ring/Ring/Calls/Conference/ContactPickerViewController.swift index 7272ea071ca9f40ff023b82c59b6565ac084fc9b..97127886aa5547a6e14c47e215d7c7350a57d53f 100644 --- a/Ring/Ring/Calls/Conference/ContactPickerViewController.swift +++ b/Ring/Ring/Calls/Conference/ContactPickerViewController.swift @@ -94,14 +94,14 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB indexPath: IndexPath, contactItem: ContactPickerSection.Item) in - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ConversationCell.self) + let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SmartListCell.self) if contactItem.contacts.count < 1 { return cell } - cell.newMessagesIndicator.isHidden = true - cell.newMessagesLabel.isHidden = true - cell.lastMessageDateLabel.isHidden = true - cell.presenceIndicator.isHidden = true + cell.newMessagesIndicator?.isHidden = true + cell.newMessagesLabel?.isHidden = true + cell.lastMessageDateLabel?.isHidden = true + cell.presenceIndicator?.isHidden = true if contactItem.contacts.count > 1 { cell.avatarView.isHidden = true var name = "" @@ -119,7 +119,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB var contact = contactItem.contacts.first! cell.nameLabel.text = contact.firstLine - cell.lastMessagePreviewLabel.text = contact.secondLine + cell.lastMessagePreviewLabel?.text = contact.secondLine var imageData: Data? if let contactProfile = contact.profile, let photo = contactProfile.photo, @@ -138,7 +138,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB .observeOn(MainScheduler.instance) .startWith(status.value) .subscribe(onNext: { precence in - cell.presenceIndicator.isHidden = !precence + cell.presenceIndicator?.isHidden = !precence }) .disposed(by: cell.disposeBag) return cell @@ -155,7 +155,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB func setupTableViews() { self.tableView.rowHeight = 64.0 self.tableView.delegate = self - self.tableView.register(cellType: ConversationCell.self) + self.tableView.register(cellType: SmartListCell.self) self.tableView.rx.itemSelected.subscribe(onNext: { [unowned self] indexPath in if let contactToAdd: ConferencableItem = try? self.tableView.rx.model(at: indexPath) { self.viewModel.addContactToConference(contact: contactToAdd) diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift index 14f5e5308b7df4320d928bf2f5b35fd1f3819682..de185106dbbe5cefbb4bff2099b90f4275bcddfa 100644 --- a/Ring/Ring/Constants/Generated/Strings.swift +++ b/Ring/Ring/Constants/Generated/Strings.swift @@ -52,14 +52,24 @@ internal enum L10n { } internal enum AccountPage { - /// Block List + /// Blocked contacts internal static let blockedContacts = L10n.tr("Localizable", "accountPage.blockedContacts") + /// After enabling booth mode all your conversations will be removed. + internal static let boothModeAlertMessage = L10n.tr("Localizable", "accountPage.boothModeAlertMessage") + /// In booth mode conversation history not saved and jami functionality limited by making outgoing calls. When you enable booth mode all your conversations will be removed. + internal static let boothModeExplanation = L10n.tr("Localizable", "accountPage.boothModeExplanation") + /// Change password + internal static let changePassword = L10n.tr("Localizable", "accountPage.changePassword") + /// Password incorrect + internal static let changePasswordError = L10n.tr("Localizable", "accountPage.changePasswordError") /// Contact me using "%s" on the Jami distributed communication platform: https://jami.net internal static func contactMeOnJamiContant(_ p1: UnsafePointer<CChar>) -> String { return L10n.tr("Localizable", "accountPage.contactMeOnJamiContant", p1) } /// Contact me on Jami! internal static let contactMeOnJamiTitle = L10n.tr("Localizable", "accountPage.contactMeOnJamiTitle") + /// Create password + internal static let createPassword = L10n.tr("Localizable", "accountPage.createPassword") /// Account Details internal static let credentialsHeader = L10n.tr("Localizable", "accountPage.credentialsHeader") /// Device revocation error @@ -78,6 +88,12 @@ internal enum L10n { internal static let deviceRevoked = L10n.tr("Localizable", "accountPage.deviceRevoked") /// Devices internal static let devicesListHeader = L10n.tr("Localizable", "accountPage.devicesListHeader") + /// Disable Booth Mode + internal static let disableBoothMode = L10n.tr("Localizable", "accountPage.disableBoothMode") + /// Pleace provide your account password + internal static let disableBoothModeExplanation = L10n.tr("Localizable", "accountPage.disableBoothModeExplanation") + /// Enable Booth Mode + internal static let enableBoothMode = L10n.tr("Localizable", "accountPage.enableBoothMode") /// Enable Notifications internal static let enableNotifications = L10n.tr("Localizable", "accountPage.enableNotifications") /// Enable Proxy @@ -86,8 +102,16 @@ internal enum L10n { internal static let linkDeviceTitle = L10n.tr("Localizable", "accountPage.linkDeviceTitle") /// Name internal static let namePlaceholder = L10n.tr("Localizable", "accountPage.namePlaceholder") + /// Confirm new password + internal static let newPasswordConfirmPlaceholder = L10n.tr("Localizable", "accountPage.newPasswordConfirmPlaceholder") + /// Enter new password + internal static let newPasswordPlaceholder = L10n.tr("Localizable", "accountPage.newPasswordPlaceholder") + /// To enable Booth mode you need to create account password first. + internal static let noBoothMode = L10n.tr("Localizable", "accountPage.noBoothMode") /// Your device won't receive notifications when proxy is disabled internal static let noProxyExplanationLabel = L10n.tr("Localizable", "accountPage.noProxyExplanationLabel") + /// Enter old password + internal static let oldPasswordPlaceholder = L10n.tr("Localizable", "accountPage.oldPasswordPlaceholder") /// Other internal static let other = L10n.tr("Localizable", "accountPage.other") /// Enter account password @@ -151,6 +175,12 @@ internal enum L10n { internal static let clearAction = L10n.tr("Localizable", "actions.clearAction") /// Delete internal static let deleteAction = L10n.tr("Localizable", "actions.deleteAction") + /// Done + internal static let doneAction = L10n.tr("Localizable", "actions.doneAction") + /// Audio Call + internal static let startAudioCall = L10n.tr("Localizable", "actions.startAudioCall") + /// Video Call + internal static let startVideoCall = L10n.tr("Localizable", "actions.startVideoCall") } internal enum Alerts { @@ -206,6 +236,14 @@ internal enum L10n { internal static let profileTakePhoto = L10n.tr("Localizable", "alerts.profileTakePhoto") /// Upload photo internal static let profileUploadPhoto = L10n.tr("Localizable", "alerts.profileUploadPhoto") + /// Record an audio message + internal static let recordAudioMessage = L10n.tr("Localizable", "alerts.recordAudioMessage") + /// Record a video message + internal static let recordVideoMessage = L10n.tr("Localizable", "alerts.recordVideoMessage") + /// Upload file + internal static let uploadFile = L10n.tr("Localizable", "alerts.uploadFile") + /// Upload photo or movie + internal static let uploadPhoto = L10n.tr("Localizable", "alerts.uploadPhoto") } internal enum BlockListPage { diff --git a/Ring/Ring/Coordinators/AppCoordinator.swift b/Ring/Ring/Coordinators/AppCoordinator.swift index 56819a918ed028cbfd5a8ed5cbfd134f31c1e0b0..567f4efc8eb32cbb754d5f0061abdba76f3fd926 100644 --- a/Ring/Ring/Coordinators/AppCoordinator.swift +++ b/Ring/Ring/Coordinators/AppCoordinator.swift @@ -35,6 +35,7 @@ public enum AppState: State { case allSet case accountRemoved case needAccountMigration(accountId: String) + case accountModeSwitched } public enum VCType: String { @@ -93,6 +94,8 @@ final class AppCoordinator: Coordinator, StateableResponsive { self.accountRemoved() case .needAccountMigration(let accountId): self.migrateAccount(accountId: accountId) + case .accountModeSwitched: + self.switchAccountMode() } }).disposed(by: self.disposeBag) } @@ -108,6 +111,10 @@ final class AppCoordinator: Coordinator, StateableResponsive { func accountRemoved() { self.tabBarViewController.selectedIndex = 0 } + func switchAccountMode() { + self.childCoordinators[0].start() + self.tabBarViewController.selectedIndex = 0 + } func migrateAccount(accountId: String) { let migratonController = MigrateAccountViewController.instantiate(with: self.injectionBag) diff --git a/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift b/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift index 5177aa0a79b5b7e522188f8ffa8aab5fa9d74477..406d96c3e56df6de7ae6bb47c43697dff43d3ae0 100644 --- a/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift +++ b/Ring/Ring/Database/DBHelpers/ConversationDataHepler.swift @@ -132,4 +132,16 @@ final class ConversationDataHelper { return false } } + + func deleteAll(dataBase: Connection) -> Bool { + do { + if try dataBase.run(table.delete()) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } } diff --git a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift index 8d57bc8aba7210bee4d9691a46244fced29c44a3..6cd723a9a562acf26f481dcb72fb489c472408e2 100644 --- a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift +++ b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift @@ -299,6 +299,18 @@ final class InteractionDataHelper { } } + func deleteAll(dataBase: Connection) -> Bool { + do { + if try dataBase.run(table.delete()) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } + func insertIfNotExist(item: Interaction, dataBase: Connection) -> Int64? { let querySelect = table.filter(conversation == item.conversation && body == item.body && diff --git a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift index 2118f85c740761a26fcad488ba0da306c1869ed6..4387db61c721912b23335fde8644f5f9f3849fdd 100644 --- a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift +++ b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift @@ -202,4 +202,16 @@ final class ProfileDataHelper { } } } + + func deleteAll(dataBase: Connection) -> Bool { + do { + if try dataBase.run(contactsProfileTable.delete()) > 0 { + return true + } else { + return false + } + } catch { + return false + } + } } diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift index 5355819dd45d6e057737132b580ae0161151a119..ad937d1007f21a1c7bde114ffbd47270c95d75e2 100644 --- a/Ring/Ring/Database/DBManager.swift +++ b/Ring/Ring/Database/DBManager.swift @@ -393,6 +393,34 @@ class DBManager { } } + func clearAllHistoryFor(accountId: String) -> Completable { + return Completable.create { [unowned self] completable in + do { + guard let dataBase = self.dbConnections.forAccount(account: accountId) else { + throw DBBridgingError.deleteConversationFailed + } + try dataBase.transaction { + if !self.interactionHepler + .deleteAll(dataBase: dataBase) { + completable(.error(DBBridgingError.deleteConversationFailed)) + } + if !self.conversationHelper + .deleteAll(dataBase: dataBase) { + completable(.error(DBBridgingError.deleteConversationFailed)) + } + if !self.profileHepler + .deleteAll(dataBase: dataBase) { + completable(.error(DBBridgingError.deleteConversationFailed)) + } + completable(.completed) + } + } catch { + completable(.error(DBBridgingError.deleteConversationFailed)) + } + return Disposables.create { } + } + } + func clearHistoryFor(accountId: String, and participantUri: String, keepConversation: Bool) -> Completable { diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift index 43e1cb94caf023118236c9b2c589e5617a04f304..89bf4dfeaca3c40e1940ad0b1b168e118f8ec397 100644 --- a/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift +++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewModel.swift @@ -91,12 +91,12 @@ class ContactRequestsViewModel: Stateable, ViewModel { } func discard(withItem item: ContactRequestItem) -> Observable<Void> { - return self.contactsService.discard(contactRequest: item.contactRequest, + return self.contactsService.discard(from: item.contactRequest.ringId, withAccountId: item.contactRequest.accountId) } func ban(withItem item: ContactRequestItem) -> Observable<Void> { - let discardCompleted = self.contactsService.discard(contactRequest: item.contactRequest, + let discardCompleted = self.contactsService.discard(from: item.contactRequest.ringId, withAccountId: item.contactRequest.accountId) guard let uri = JamiURI.init(schema: URIType.ring, infoHach: item.contactRequest.ringId) diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index 1e0959673ea69ec335bb603600eec0b6ef8ae79b..e5a2a19b03a90718dd665757fea7f076a91b0c73 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -155,13 +155,13 @@ class ConversationViewModel: Stateable, ViewModel { self.contactsService .getContactRequestVCard(forContactWithRingId: self.conversation.value.hash) - .subscribe(onSuccess: { vCard in + .subscribe(onSuccess: { [weak self] vCard in guard let imageData = vCard.imageData else { - self.log.warning("vCard for ringId: \(contactUri) has no image") + self?.log.warning("vCard for ringId: \(contactUri) has no image") return } - self.profileImageData.value = imageData - self.displayName.value = VCardUtils.getName(from: vCard) + self?.profileImageData.value = imageData + self?.displayName.value = VCardUtils.getName(from: vCard) }) .disposed(by: self.disposeBag) @@ -202,8 +202,18 @@ class ConversationViewModel: Stateable, ViewModel { .contactPresence[self.conversation.value.hash] { self.contactPresence = contactPresence } else { - self.log.warning("Contact presence unknown for: \(contactUri)") self.contactPresence.value = false + presenceService.sharedResponseStream + .filter({ [weak self] serviceEvent in + guard let uri: String = serviceEvent + .getEventInput(ServiceEventInput.uri), + let accountID: String = serviceEvent + .getEventInput(ServiceEventInput.accountId) else {return false} + return uri == self?.conversation.value.hash && + accountID == self?.conversation.value.accountId + }).subscribe(onNext: { [weak self] _ in + self?.subscribePresence() + }).disposed(by: self.disposeBag) } if let contactUserName = contact?.userName { @@ -212,10 +222,10 @@ class ConversationViewModel: Stateable, ViewModel { self.userName.value = self.conversation.value.hash // Return an observer for the username lookup self.nameService.usernameLookupStatus - .filter({ lookupNameResponse in + .filter({ [weak self] lookupNameResponse in return lookupNameResponse.address != nil && (lookupNameResponse.address == contactUri || - lookupNameResponse.address == self.conversation.value.hash) + lookupNameResponse.address == self?.conversation.value.hash) }).subscribe(onNext: { [weak self] lookupNameResponse in if let name = lookupNameResponse.name, !name.isEmpty { self?.userName.value = name @@ -238,6 +248,15 @@ class ConversationViewModel: Stateable, ViewModel { } } + func subscribePresence() { + if let contactPresence = self.presenceService + .contactPresence[self.conversation.value.hash] { + self.contactPresence = contactPresence + } else { + self.contactPresence.value = false + } + } + //Displays the entire date ( for messages received before the current week ) private let dateFormatter = DateFormatter() @@ -412,6 +431,9 @@ class ConversationViewModel: Stateable, ViewModel { }, onError: { [weak self] (error) in self?.log.info(error) }).disposed(by: self.disposeBag) + self.presenceService.subscribeBuddy(withAccountId: currentAccount.id, + withUri: self.conversation.value.hash, + withFlag: true) } func block() { @@ -422,7 +444,7 @@ class ConversationViewModel: Stateable, ViewModel { ban: true, withAccountId: accountId) if let contactRequest = self.contactsService.contactRequest(withRingId: contactRingId) { - let discardCompleted = self.contactsService.discard(contactRequest: contactRequest, + let discardCompleted = self.contactsService.discard(from: contactRequest.ringId, withAccountId: accountId) blockComplete = Observable<Void>.zip(discardCompleted, removeCompleted) { _, _ in return @@ -443,7 +465,7 @@ class ConversationViewModel: Stateable, ViewModel { func ban(withItem item: ContactRequestItem) -> Observable<Void> { let accountId = item.contactRequest.accountId - let discardCompleted = self.contactsService.discard(contactRequest: item.contactRequest, + let discardCompleted = self.contactsService.discard(from: item.contactRequest.ringId, withAccountId: accountId) let removeCompleted = self.contactsService.removeContact(withUri: item.contactRequest.ringId, ban: true, diff --git a/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift index ff120c130e0fd2e28fd555a52cefbda5f2456321..bbbbabef4095c9021f56fb4868545fa6429e016a 100644 --- a/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift +++ b/Ring/Ring/Features/Conversations/ConversationsCoordinator.swift @@ -96,6 +96,11 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa guard let account = self.accountService .getAccount(fromAccountId: call.accountId), !call.callId.isEmpty else {return} + if self.accountService.boothMode() { + self.callService.refuse(callId: call.callId) + .subscribe().disposed(by: self.disposeBag) + return + } guard let topController = getTopController(), !topController.isKind(of: (CallViewController).self) else { return @@ -213,7 +218,14 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa self.pushConversation(withConversationViewModel: conversationViewModel) } - func start () { + func start() { + self.navigationViewController.viewControllers.removeAll() + let boothMode = self.accountService.boothMode() + if boothMode { + let smartListViewController = IncognitoSmartListViewController.instantiate(with: self.injectionBag) + self.present(viewController: smartListViewController, withStyle: .show, withAnimation: true, withStateable: smartListViewController.viewModel) + return + } let smartListViewController = SmartlistViewController.instantiate(with: self.injectionBag) self.present(viewController: smartListViewController, withStyle: .show, withAnimation: true, withStateable: smartListViewController.viewModel) } diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift index e2f4ddd11ebf85f3a60335e7295253ec22de7ad8..1c6c8e0917d8c624d8cef90266ae8243ae559cde 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift @@ -28,11 +28,13 @@ class ConversationCell: UITableViewCell, NibReusable { @IBOutlet weak var avatarView: UIView! @IBOutlet weak var nameLabel: UILabel! - @IBOutlet weak var newMessagesIndicator: UIView! - @IBOutlet weak var newMessagesLabel: UILabel! - @IBOutlet weak var lastMessageDateLabel: UILabel! - @IBOutlet weak var lastMessagePreviewLabel: UILabel! - @IBOutlet weak var presenceIndicator: UIView! + @IBOutlet weak var newMessagesIndicator: UIView? + @IBOutlet weak var newMessagesLabel: UILabel? + @IBOutlet weak var lastMessageDateLabel: UILabel? + @IBOutlet weak var lastMessagePreviewLabel: UILabel? + @IBOutlet weak var presenceIndicator: UIView? + + var avatarSize: CGFloat { return 40 } override func setSelected(_ selected: Bool, animated: Bool) { self.backgroundColor = UIColor.jamiUITableViewCellSelection @@ -76,21 +78,23 @@ class ConversationCell: UITableViewCell, NibReusable { .addSubview( AvatarView(profileImageData: profileData.element?.0, username: data, - size: 40)) + size: self?.avatarSize ?? 40)) return }) .disposed(by: self.disposeBag) // unread messages - self.newMessagesLabel.text = item.unreadMessages - self.newMessagesIndicator.isHidden = item.hideNewMessagesLabel + self.newMessagesLabel?.text = item.unreadMessages + self.newMessagesIndicator?.isHidden = item.hideNewMessagesLabel // presence + if self.presenceIndicator != nil { item.contactPresence.asObservable() .observeOn(MainScheduler.instance) .map { value in !value } - .bind(to: self.presenceIndicator.rx.isHidden) + .bind(to: self.presenceIndicator!.rx.isHidden) .disposed(by: self.disposeBag) + } // username item.bestName.asObservable() @@ -100,11 +104,11 @@ class ConversationCell: UITableViewCell, NibReusable { self.nameLabel.lineBreakMode = .byTruncatingTail // last message date - self.lastMessageDateLabel.text = item.lastMessageReceivedDate + self.lastMessageDateLabel?.text = item.lastMessageReceivedDate // last message preview - self.lastMessagePreviewLabel.text = item.lastMessage - self.lastMessagePreviewLabel.lineBreakMode = .byTruncatingTail + self.lastMessagePreviewLabel?.text = item.lastMessage + self.lastMessagePreviewLabel?.lineBreakMode = .byTruncatingTail self.selectionStyle = .none } diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..b7357ec87a0758def4eb6f79700388b77b42ea94 --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.swift @@ -0,0 +1,23 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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. +*/ + +class IncognitoSmartListCell: ConversationCell { + override var avatarSize: CGFloat { return 80 } +} diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.xib b/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.xib new file mode 100644 index 0000000000000000000000000000000000000000..93f1729e58c6beb471f7308a8ad56d58043d29b4 --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/IncognitoSmartListCell.xib @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> + <device id="retina6_1" orientation="portrait" appearance="light"/> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> + <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 contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="150" id="1LM-2m-16A" customClass="IncognitoSmartListCell" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="414" height="150"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="1LM-2m-16A" id="dGp-y2-lQ3"> + <rect key="frame" x="0.0" y="0.0" width="414" height="150"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4Fr-fc-JfP" userLabel="Avatar View"> + <rect key="frame" x="167" y="20" width="80" height="80"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="width" constant="80" id="iSv-Ly-cn7"/> + <constraint firstAttribute="height" constant="80" id="mQv-lf-YGw"/> + </constraints> + </view> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c3C-hG-z8X"> + <rect key="frame" x="207" y="110" width="0.0" height="30"/> + <fontDescription key="fontDescription" type="system" weight="light" pointSize="20"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstItem="4Fr-fc-JfP" firstAttribute="top" secondItem="dGp-y2-lQ3" secondAttribute="top" constant="20" id="1nI-Wm-UXW"/> + <constraint firstItem="c3C-hG-z8X" firstAttribute="top" secondItem="4Fr-fc-JfP" secondAttribute="bottom" constant="10" id="Vv1-sF-jKg"/> + <constraint firstItem="4Fr-fc-JfP" firstAttribute="centerX" secondItem="dGp-y2-lQ3" secondAttribute="centerX" id="egs-ar-EXo"/> + <constraint firstItem="c3C-hG-z8X" firstAttribute="centerX" secondItem="dGp-y2-lQ3" secondAttribute="centerX" id="iHj-r4-1Yn"/> + <constraint firstAttribute="bottom" secondItem="c3C-hG-z8X" secondAttribute="bottom" constant="10" id="wAQ-Fs-WEW"/> + </constraints> + </tableViewCellContentView> + <connections> + <outlet property="avatarView" destination="4Fr-fc-JfP" id="HGs-MF-iXG"/> + <outlet property="nameLabel" destination="c3C-hG-z8X" id="AZC-rO-J23"/> + </connections> + <point key="canvasLocation" x="9" y="-89"/> + </tableViewCell> + </objects> +</document> diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..0a839bea12db498f355b48945fcae4e0b97ae591 --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.swift @@ -0,0 +1,22 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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. +*/ + +class SmartListCell: ConversationCell { +} diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib b/Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.xib similarity index 95% rename from Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib rename to Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.xib index 37b692c860786ef266a3bab9b9dd0e2b6266c147..d08b5aad89fec8fac521c69ada8abf2ebdac3686 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/SmartListCell.xib @@ -1,21 +1,19 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> - <device id="retina4_7" orientation="portrait"> - <adaptation id="fullscreen"/> - </device> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> + <device id="retina4_7" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/> <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 contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="76" id="KGk-i7-Jjw" customClass="ConversationCell" customModule="Ring" customModuleProvider="target"> + <tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="76" id="KGk-i7-Jjw" customClass="SmartListCell" customModule="Ring" customModuleProvider="target"> <rect key="frame" x="0.0" y="0.0" width="358" height="76"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM"> - <rect key="frame" x="0.0" y="0.0" width="358" height="75.5"/> + <rect key="frame" x="0.0" y="0.0" width="358" height="76"/> <autoresizingMask key="autoresizingMask"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KC4-sG-yj8" userLabel="Avatar View"> @@ -33,13 +31,13 @@ <nil key="highlightedColor"/> </label> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2fJ-Wf-1e0"> - <rect key="frame" x="60" y="8" width="278" height="55.5"/> + <rect key="frame" x="60" y="8" width="278" height="56"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="14"/> <nil key="textColor"/> <nil key="highlightedColor"/> </label> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eug-ak-r49"> - <rect key="frame" x="60" y="67.5" width="278" height="0.0"/> + <rect key="frame" x="60" y="68" width="278" height="0.0"/> <fontDescription key="fontDescription" type="system" pointSize="14"/> <nil key="textColor"/> <nil key="highlightedColor"/> diff --git a/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.storyboard b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.storyboard new file mode 100644 index 0000000000000000000000000000000000000000..459e12ef8c1f68be24e308fb5d49b660f1e66f26 --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.storyboard @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="VQC-aV-7oi"> + <device id="retina6_1" orientation="portrait" appearance="light"/> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Incognito Smart List View Controller--> + <scene sceneID="ddP-29-OKS"> + <objects> + <viewController hidesBottomBarWhenPushed="YES" id="VQC-aV-7oi" customClass="IncognitoSmartListViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="Lsh-nl-RJT"> + <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="XVO-MU-jo2"> + <rect key="frame" x="0.0" y="44" width="414" height="852"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Snl-Rn-2k3"> + <rect key="frame" x="0.0" y="0.0" width="414" height="236"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="byS-Ap-EAU"> + <rect key="frame" x="0.0" y="0.0" width="414" height="40"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="height" constant="40" id="hRo-xA-uvQ"/> + </constraints> + </view> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="jamiIcon" translatesAutoresizingMaskIntoConstraints="NO" id="thK-sI-jCE"> + <rect key="frame" x="0.0" y="40" width="414" height="105"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="height" constant="105" id="wJk-Kp-hds"/> + </constraints> + </imageView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0Gt-Rt-p98"> + <rect key="frame" x="0.0" y="145" width="414" height="35"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="height" constant="35" id="roG-sR-QtB"/> + </constraints> + </view> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qeo-Fd-n47" userLabel="Network Alert View"> + <rect key="frame" x="0.0" y="180" width="414" height="56"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KdJ-ct-8fq" userLabel="Alert Labels View"> + <rect key="frame" x="190.5" y="0.0" width="33" height="56"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="X9t-3C-1Cp" userLabel="Network Alert Label"> + <rect key="frame" x="-2" y="19" width="37.5" height="18"/> + <fontDescription key="fontDescription" type="system" pointSize="15"/> + <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstItem="X9t-3C-1Cp" firstAttribute="width" secondItem="KdJ-ct-8fq" secondAttribute="width" multiplier="1.13636" id="GvM-sR-tIj"/> + <constraint firstItem="X9t-3C-1Cp" firstAttribute="centerX" secondItem="KdJ-ct-8fq" secondAttribute="centerX" id="JP1-Ha-1s8"/> + <constraint firstAttribute="height" constant="56" id="axB-4g-Day"/> + <constraint firstItem="X9t-3C-1Cp" firstAttribute="centerY" secondItem="KdJ-ct-8fq" secondAttribute="centerY" id="w27-o3-a8S"/> + </constraints> + </view> + </subviews> + <color key="backgroundColor" red="0.94117647059999998" green="0.43921568630000002" blue="0.43921568630000002" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstAttribute="height" constant="56" id="F1g-wg-LFx"/> + <constraint firstItem="KdJ-ct-8fq" firstAttribute="centerY" secondItem="qeo-Fd-n47" secondAttribute="centerY" id="mdH-co-Kac"/> + <constraint firstItem="KdJ-ct-8fq" firstAttribute="centerX" secondItem="qeo-Fd-n47" secondAttribute="centerX" priority="750" id="pCf-7n-IeF"/> + </constraints> + </view> + </subviews> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="trailing" secondItem="qeo-Fd-n47" secondAttribute="trailing" id="6ct-Kx-hXb"/> + <constraint firstItem="qeo-Fd-n47" firstAttribute="leading" secondItem="Snl-Rn-2k3" secondAttribute="leading" id="nP8-db-kad"/> + </constraints> + </stackView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RC3-OR-Sdo"> + <rect key="frame" x="0.0" y="236" width="414" height="56"/> + <subviews> + <searchBar contentMode="redraw" id="rXO-wH-W8B"> + <rect key="frame" x="0.0" y="0.0" width="414" height="56"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <textInputTraits key="textInputTraits"/> + </searchBar> + </subviews> + <constraints> + <constraint firstAttribute="height" constant="56" id="i5V-8X-L7Z"/> + </constraints> + </view> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Yfv-j3-kOe"> + <rect key="frame" x="0.0" y="292" width="414" height="499"/> + <subviews> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="MK3-LU-grM"> + <rect key="frame" x="0.0" y="0.0" width="414" height="499"/> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <stackView key="tableFooterView" opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="15" id="j2Q-e4-mDm"> + <rect key="frame" x="0.0" y="0.0" width="414" height="44"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Syd-wf-zvr"> + <rect key="frame" x="0.0" y="0.0" width="199.5" height="44"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KVW-Ib-pta"> + <rect key="frame" x="0.0" y="0.0" width="15" height="44"/> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <constraints> + <constraint firstAttribute="width" constant="15" id="Ia9-pu-KkS"/> + </constraints> + </view> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Z1C-QN-Ish" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="15" y="0.0" width="184.5" height="44"/> + <fontDescription key="fontDescription" type="system" weight="light" pointSize="19"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal" title=" Video Call" image="video_running"> + <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + </subviews> + </stackView> + <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z0k-xY-9Qd"> + <rect key="frame" x="214.5" y="0.0" width="199.5" height="44"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5PU-M8-7lD" customClass="DesignableButton" customModule="Ring" customModuleProvider="target"> + <rect key="frame" x="0.0" y="0.0" width="184.5" height="44"/> + <fontDescription key="fontDescription" type="system" weight="light" pointSize="19"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal" title=" Audio Call" image="call_button"> + <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </state> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </button> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="b0i-TS-FoA"> + <rect key="frame" x="184.5" y="0.0" width="15" height="44"/> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <constraints> + <constraint firstAttribute="width" constant="15" id="tMR-m6-i2R"/> + </constraints> + </view> + </subviews> + </stackView> + </subviews> + </stackView> + </tableView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ub7-pM-ati"> + <rect key="frame" x="187.5" y="0.0" width="39.5" height="19.5"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <constraints> + <constraint firstItem="Ub7-pM-ati" firstAttribute="top" secondItem="MK3-LU-grM" secondAttribute="top" id="BZv-Hr-4Rb"/> + <constraint firstItem="MK3-LU-grM" firstAttribute="top" secondItem="Yfv-j3-kOe" secondAttribute="top" id="Huf-lP-gNy"/> + <constraint firstItem="Ub7-pM-ati" firstAttribute="centerX" secondItem="Yfv-j3-kOe" secondAttribute="centerX" id="bYV-2e-sgt"/> + <constraint firstAttribute="bottom" secondItem="MK3-LU-grM" secondAttribute="bottom" id="fTV-NI-0xg"/> + </constraints> + </view> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gvx-Bj-xi3"> + <rect key="frame" x="0.0" y="791" width="414" height="41"/> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <constraints> + <constraint firstAttribute="height" constant="41" id="YSq-hX-dDN"/> + </constraints> + <fontDescription key="fontDescription" type="system" weight="light" pointSize="22"/> + </button> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3Uk-bD-yCE"> + <rect key="frame" x="0.0" y="832" width="414" height="20"/> + <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <constraints> + <constraint firstAttribute="height" constant="20" id="3Ui-l5-o0o"/> + </constraints> + </view> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="RC3-OR-Sdo" secondAttribute="trailing" id="3Ha-ZA-evo"/> + <constraint firstItem="Yfv-j3-kOe" firstAttribute="top" secondItem="RC3-OR-Sdo" secondAttribute="bottom" id="FQl-vD-D1e"/> + <constraint firstItem="RC3-OR-Sdo" firstAttribute="leading" secondItem="XVO-MU-jo2" secondAttribute="leading" id="blv-zx-TKT"/> + <constraint firstItem="RC3-OR-Sdo" firstAttribute="top" secondItem="Snl-Rn-2k3" secondAttribute="bottom" id="uPc-9e-hq4"/> + </constraints> + </stackView> + </subviews> + <constraints> + <constraint firstItem="MK3-LU-grM" firstAttribute="leading" secondItem="Jq5-jG-gOI" secondAttribute="leading" id="3kE-pp-Qn5"/> + <constraint firstItem="XVO-MU-jo2" firstAttribute="trailing" secondItem="Lsh-nl-RJT" secondAttribute="trailing" id="6sJ-Co-Dgz"/> + <constraint firstItem="Jq5-jG-gOI" firstAttribute="trailing" secondItem="MK3-LU-grM" secondAttribute="trailing" id="Ng1-ld-WkW"/> + <constraint firstAttribute="bottom" secondItem="XVO-MU-jo2" secondAttribute="bottom" id="gDO-q2-aT0"/> + <constraint firstItem="Jq5-jG-gOI" firstAttribute="top" secondItem="XVO-MU-jo2" secondAttribute="top" id="uT3-xA-kDy"/> + <constraint firstItem="XVO-MU-jo2" firstAttribute="leading" secondItem="Lsh-nl-RJT" secondAttribute="leading" id="y8D-R2-hpu"/> + </constraints> + <viewLayoutGuide key="safeArea" id="Jq5-jG-gOI"/> + </view> + <connections> + <outlet property="boothSwitch" destination="gvx-Bj-xi3" id="SSi-NS-vkg"/> + <outlet property="logoView" destination="Snl-Rn-2k3" id="0xv-XO-JFo"/> + <outlet property="networkAlertLabel" destination="X9t-3C-1Cp" id="ruo-Cn-5ax"/> + <outlet property="networkAlertView" destination="qeo-Fd-n47" id="C8H-FJ-Co6"/> + <outlet property="placeAudioCall" destination="5PU-M8-7lD" id="DuF-do-s0o"/> + <outlet property="placeVideoCall" destination="Z1C-QN-Ish" id="EGx-dE-zBT"/> + <outlet property="searchBarShadow" destination="RC3-OR-Sdo" id="oCc-BH-udN"/> + <outlet property="searchView" destination="46f-Ue-tcu" id="cja-31-Vaz"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="mvQ-Si-Of0" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> + <customObject id="46f-Ue-tcu" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target"> + <connections> + <outlet property="searchBar" destination="rXO-wH-W8B" id="r63-1p-hRj"/> + <outlet property="searchResultsTableView" destination="MK3-LU-grM" id="7Lz-ew-QeT"/> + <outlet property="searchingLabel" destination="Ub7-pM-ati" id="R9B-4d-mA5"/> + </connections> + </customObject> + </objects> + <point key="canvasLocation" x="137.68115942028987" y="91.741071428571431"/> + </scene> + </scenes> + <resources> + <image name="call_button" width="29" height="29"/> + <image name="jamiIcon" width="100" height="95"/> + <image name="video_running" width="29" height="29"/> + </resources> +</document> diff --git a/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..598a083b093cec67f7356cd4a07dd1e7d41af37f --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift @@ -0,0 +1,204 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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 RxDataSources +import RxCocoa +import Reusable +import PKHUD + +class IncognitoSmartListViewController: UIViewController, StoryboardBased, ViewModelBased { + + @IBOutlet weak var searchView: JamiSearchView! + + @IBOutlet weak var placeVideoCall: DesignableButton! + @IBOutlet weak var placeAudioCall: DesignableButton! + @IBOutlet weak var logoView: UIStackView! + @IBOutlet weak var boothSwitch: UIButton! + @IBOutlet weak var networkAlertLabel: UILabel! + @IBOutlet weak var networkAlertView: UIView! + @IBOutlet weak var searchBarShadow: UIView! + + var viewModel: IncognitoSmartListViewModel! + fileprivate let disposeBag = DisposeBag() + + override func viewDidLoad() { + super.viewDidLoad() + self.configureRingNavigationBar() + self.setupSearchBar() + searchView.configure(with: viewModel.injectionBag, source: viewModel, isIncognito: true) + self.setupUI() + self.applyL10n() + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(withNotification:)), name: UIResponder.keyboardDidShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(withNotification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + self.tabBarController?.tabBar.isHidden = true + self.tabBarController?.tabBar.layer.zPosition = -1 + NotificationCenter.default.rx + .notification(UIDevice.orientationDidChangeNotification) + .observeOn(MainScheduler.instance) + .subscribe(onNext: {[weak self](_) in + self?.placeVideoCall.updateGradientFrame() + self?.placeAudioCall.updateGradientFrame() + }).disposed(by: self.disposeBag) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: animated) + } + + func applyL10n() { + self.navigationItem.title = "" + self.networkAlertLabel.text = L10n.Smartlist.noNetworkConnectivity + boothSwitch.setTitle(L10n.AccountPage.disableBoothMode, for: .normal) + placeAudioCall.setTitle(L10n.Actions.startAudioCall, for: .normal) + placeVideoCall.setTitle(L10n.Actions.startVideoCall, for: .normal) + } + + func setupUI() { + view.backgroundColor = UIColor.jamiBackgroundSecondaryColor + self.placeVideoCall.applyGradient(with: [UIColor.jamiButtonLight, UIColor.jamiButtonDark], gradient: .horizontal) + self.placeAudioCall.applyGradient(with: [UIColor.jamiButtonLight, UIColor.jamiButtonDark], gradient: .horizontal) + self.boothSwitch.setTitleColor(.jamiTextSecondary, for: .normal) + self.searchView.editSearch + .subscribe(onNext: {[weak self] (editing) in + self?.logoView.isHidden = editing + self?.boothSwitch.isHidden = editing + }).disposed(by: disposeBag) + + self.placeVideoCall.rx.tap + .subscribe(onNext: { [weak self] in + self?.viewModel.startCall(audioOnly: false) + }).disposed(by: self.disposeBag) + + self.placeAudioCall.rx.tap + .subscribe(onNext: { [weak self] in + self?.viewModel.startCall(audioOnly: true) + }).disposed(by: self.disposeBag) + self.boothSwitch.rx.tap + .subscribe(onNext: { [weak self] in + self?.confirmBoothModeAlert() + }).disposed(by: self.disposeBag) + let isHidden = self.viewModel.networkConnectionState() == .none ? false : true + self.networkAlertView.isHidden = isHidden + self.viewModel.connectionState + .subscribe(onNext: { [weak self] connectionState in + let isHidden = connectionState == .none ? false : true + self?.networkAlertView.isHidden = isHidden + }) + .disposed(by: self.disposeBag) + } + + @objc func keyboardWillShow(withNotification notification: Notification) { + guard let userInfo: Dictionary = notification.userInfo else {return} + guard let keyboardFrame: NSValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + let keyboardRectangle = keyboardFrame.cgRectValue + let keyboardHeight = keyboardRectangle.height + guard let tabBarHeight = (self.tabBarController?.tabBar.frame.size.height) else { + return + } + self.searchView.searchResultsTableView.contentInset.bottom = keyboardHeight - tabBarHeight + self.searchView.searchResultsTableView.scrollIndicatorInsets.bottom = keyboardHeight - tabBarHeight + } + + @objc func keyboardWillHide(withNotification notification: Notification) { + self.searchView.searchResultsTableView.contentInset.bottom = 0 + self.searchView.searchResultsTableView.scrollIndicatorInsets.bottom = 0 + } + + func setupSearchBar() { + searchBarShadow.backgroundColor = UIColor.jamiBackgroundSecondaryColor + searchBarShadow.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor + searchBarShadow.layer.shadowOffset = CGSize(width: 0.0, height: 1.5) + searchBarShadow.layer.shadowOpacity = 0.2 + searchBarShadow.layer.shadowRadius = 3 + searchBarShadow.layer.masksToBounds = false + searchBarShadow.superview?.bringSubviewToFront(searchBarShadow) + + if #available(iOS 13.0, *) { + let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial)) + visualEffectView.frame = searchBarShadow.bounds + visualEffectView.isUserInteractionEnabled = false + searchBarShadow.insertSubview(visualEffectView, at: 0) + visualEffectView.translatesAutoresizingMaskIntoConstraints = false + visualEffectView.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: 0).isActive = true + visualEffectView.trailingAnchor.constraint(equalTo: searchBarShadow.trailingAnchor, constant: 0).isActive = true + visualEffectView.leadingAnchor.constraint(equalTo: searchBarShadow.leadingAnchor, constant: 0).isActive = true + visualEffectView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true + visualEffectView.bottomAnchor.constraint(equalTo: searchBarShadow.bottomAnchor, constant: 0).isActive = true + } else { + let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + visualEffectView.frame = searchBarShadow.bounds + visualEffectView.isUserInteractionEnabled = false + let background = UIView() + background.frame = searchBarShadow.bounds + background.backgroundColor = UIColor(red: 245, green: 245, blue: 245, alpha: 1.0) + background.alpha = 0.7 + searchBarShadow.insertSubview(background, at: 0) + searchBarShadow.insertSubview(visualEffectView, at: 0) + background.translatesAutoresizingMaskIntoConstraints = false + visualEffectView.translatesAutoresizingMaskIntoConstraints = false + visualEffectView.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: 0).isActive = true + background.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: 0).isActive = true + visualEffectView.trailingAnchor.constraint(equalTo: searchBarShadow.trailingAnchor, constant: 0).isActive = true + background.trailingAnchor.constraint(equalTo: searchBarShadow.trailingAnchor, constant: 0).isActive = true + visualEffectView.leadingAnchor.constraint(equalTo: searchBarShadow.leadingAnchor, constant: 0).isActive = true + background.leadingAnchor.constraint(equalTo: searchBarShadow.leadingAnchor, constant: 0).isActive = true + visualEffectView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true + background.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true + visualEffectView.bottomAnchor.constraint(equalTo: searchBarShadow.bottomAnchor, constant: 0).isActive = true + background.bottomAnchor.constraint(equalTo: searchBarShadow.bottomAnchor, constant: 0).isActive = true + } + logoView.superview?.bringSubviewToFront(logoView) + } + + let boothConfirmation = ConfirmationAlert() + + func confirmBoothModeAlert() { + boothConfirmation.configure(title: L10n.AccountPage.disableBoothMode, + msg: "", + enable: false, presenter: self, + disposeBag: self.disposeBag) + } +} + +extension IncognitoSmartListViewController: BoothModeConfirmationPresenter { + func enableBoothMode(enable: Bool, password: String) -> Bool { + return self.viewModel.enableBoothMode(enable: enable, password: password) + } + + func switchBoothModeState(state: Bool) { + } + + internal func stopLoadingView() { + HUD.hide(animated: false) + } + + internal func showLoadingViewWithoutText() { + HUD.show(.labeledProgress(title: "", subtitle: nil)) + } +} diff --git a/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..d66a1f7612131e1a1764d3604ffee936e41cd7ed --- /dev/null +++ b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewModel.swift @@ -0,0 +1,115 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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 +import RxCocoa + +class IncognitoSmartListViewModel: Stateable, ViewModel, FilterConversationDataSource { + + // MARK: - Rx Stateable + private let stateSubject = PublishSubject<State>() + lazy var state: Observable<State> = { + return self.stateSubject.asObservable() + }() + + fileprivate let disposeBag = DisposeBag() + + //Services + fileprivate let accountService: AccountsService + fileprivate let networkService: NetworkService + fileprivate let contactService: ContactsService + let conversationService: ConversationsService + + lazy var currentAccount: AccountModel? = { + return self.accountService.currentAccount + }() + + fileprivate var lookupName = BehaviorRelay<String?>(value: "") + + var searching = PublishSubject<Bool>() + + var connectionState = PublishSubject<ConnectionType>() + var conversationViewModels = [ConversationViewModel]() + + func networkConnectionState() -> ConnectionType { + return self.networkService.connectionState.value + } + let injectionBag: InjectionBag + + required init(with injectionBag: InjectionBag) { + self.accountService = injectionBag.accountService + self.networkService = injectionBag.networkService + self.contactService = injectionBag.contactsService + self.conversationService = injectionBag.conversationsService + self.injectionBag = injectionBag + self.networkService.connectionStateObservable + .subscribe(onNext: { [weak self] value in + self?.connectionState.onNext(value) + }) + .disposed(by: self.disposeBag) + } + + func conversationFound(conversation: ConversationViewModel?, name: String) { + contactFoundConversation.value = conversation + lookupName.accept(name) + } + fileprivate var contactFoundConversation = Variable<ConversationViewModel?>(nil) + + func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) { + } + + func enableBoothMode(enable: Bool, password: String) -> Bool { + guard let accountId = self.accountService.currentAccount?.id else { + return false + } + let result = self.accountService.setBoothMode(forAccount: accountId, enable: enable, password: password) + if !result { + return false + } + self.contactService.removeAllContacts(for: accountId) + self.conversationService + .getConversationsForAccount(accountId: accountId) + .subscribe() + .disposed(by: self.disposeBag) + self.stateSubject.onNext(ConversationState.accountModeChanged) + return true + } + + func startCall(audioOnly: Bool) { + guard let currentAccount = self.accountService.currentAccount, + let conversation = self.contactFoundConversation.value?.conversation.value else { + return + } + let username: String = lookupName.value ?? "" + self.contactService + .sendContactRequest(toContactRingId: self.contactFoundConversation.value!.conversation.value.hash, + withAccount: currentAccount) + .subscribe(onCompleted: { [weak self, weak conversation] in + guard let self = self, let conversation = conversation else { + return + } + if audioOnly { + self.stateSubject.onNext(ConversationState.startAudioCall(contactRingId: conversation.hash, userName: username)) + return + } + self.stateSubject.onNext(ConversationState.startCall(contactRingId: conversation.hash, userName: username)) + }).disposed(by: self.disposeBag) + } +} diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard index e3d4df2866023809ca3c3c936409c54167be9694..ae85950f3d9be972541a80ede3dfb7813b112105 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard @@ -233,16 +233,21 @@ <outlet property="phoneBookButton" destination="RSG-bY-flb" id="Yjw-Va-7c1"/> <outlet property="qrScanButton" destination="Eta-uf-Ija" id="Hh0-vy-8EK"/> <outlet property="scanButtonLeadingConstraint" destination="O7M-He-7UH" id="cvF-Wd-TJT"/> - <outlet property="searchBar" destination="xPr-nI-I35" id="Y3U-rV-yfc"/> <outlet property="searchBarShadow" destination="DKd-eF-L6f" id="CKZ-ws-ag1"/> - <outlet property="searchResultsTableView" destination="opE-y7-3Rm" id="F3g-9d-IQt"/> - <outlet property="searchTableViewLabel" destination="HGv-QU-VSD" id="cVs-pr-n1f"/> + <outlet property="searchView" destination="Y4B-5f-ij4" id="FtS-9R-atZ"/> <outlet property="settingsButton" destination="iaz-fd-fEz" id="R2O-R8-BDk"/> <outlet property="tableTopConstraint" destination="TVk-tz-qtF" id="lIj-Yu-ZL7"/> <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"/> + <customObject id="Y4B-5f-ij4" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target"> + <connections> + <outlet property="searchBar" destination="xPr-nI-I35" id="M1f-Qz-kHV"/> + <outlet property="searchResultsTableView" destination="opE-y7-3Rm" id="cMo-3b-5FA"/> + <outlet property="searchingLabel" destination="HGv-QU-VSD" id="WnL-0s-Szs"/> + </connections> + </customObject> </objects> <point key="canvasLocation" x="-108" y="-1208.5457271364319"/> </scene> diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift index 6a0a9f6289a2fb3a5dac65db86b55bbf0413984a..0f4c30bdc2e2898748003e0f5874b5b066324ff4 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift @@ -30,7 +30,7 @@ import ContactsUI import QuartzCore //Constants -private struct SmartlistConstants { +struct SmartlistConstants { static let smartlistRowHeight: CGFloat = 64.0 static let tableHeaderViewHeight: CGFloat = 142.0 static let firstSectionHeightForHeader: CGFloat = 51.0 @@ -40,7 +40,6 @@ private struct SmartlistConstants { } // swiftlint:disable type_body_length -// swiftlint:disable file_length class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased { private let log = SwiftyBeaver.self @@ -48,11 +47,8 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased // MARK: outlets @IBOutlet weak var tableView: UITableView! @IBOutlet weak var conversationsTableView: UITableView! - @IBOutlet weak var searchResultsTableView: UITableView! - @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var noConversationsView: UIView! @IBOutlet weak var noConversationLabel: UILabel! - @IBOutlet weak var searchTableViewLabel: UILabel! @IBOutlet weak var networkAlertLabel: UILabel! @IBOutlet weak var cellularAlertLabel: UILabel! @IBOutlet weak var networkAlertViewTopConstraint: NSLayoutConstraint! @@ -65,6 +61,7 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased @IBOutlet weak var phoneBookButton: UIButton! @IBOutlet weak var scanButtonLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var networkAlertView: UIView! + @IBOutlet weak var searchView: JamiSearchView! // account selection var accounPicker = UIPickerView() @@ -85,8 +82,9 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased override func viewDidLoad() { super.viewDidLoad() + searchView.configure(with: viewModel.injectionBag, source: viewModel, isIncognito: false) self.setupDataSources() - self.setupTableViews() + self.setupTableView() self.setupSearchBar() self.setupUI() self.applyL10n() @@ -100,6 +98,9 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased */ NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(withNotification:)), name: UIResponder.keyboardDidShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(withNotification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + self.tabBarController?.tabBar.isHidden = false + self.tabBarController?.tabBar.layer.zPosition = -0 + self.extendedLayoutIncludesOpaqueBars = true } @objc func dismissKeyboard() { @@ -120,12 +121,14 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased .titleTextAttributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Light", size: 25)!, NSAttributedString.Key.foregroundColor: UIColor.jamiMain] self.viewModel.closeAllPlayers() + self.tabBarController?.tabBar.isHidden = false + self.tabBarController?.tabBar.layer.zPosition = -0 + self.navigationController?.setNavigationBarHidden(false, animated: animated) } func applyL10n() { self.navigationItem.title = L10n.Global.homeTabBarTitle noConversationLabel.text = L10n.Smartlist.noConversation - self.searchBar.placeholder = L10n.Smartlist.searchBarPlaceholder self.networkAlertLabel.text = L10n.Smartlist.noNetworkConnectivity self.cellularAlertLabel.text = L10n.Smartlist.cellularAccess } @@ -134,11 +137,9 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased func setupUI() { view.backgroundColor = UIColor.jamiBackgroundColor conversationsTableView.backgroundColor = UIColor.jamiBackgroundColor - searchResultsTableView.backgroundColor = UIColor.jamiBackgroundColor noConversationsView.backgroundColor = UIColor.jamiBackgroundColor noConversationLabel.backgroundColor = UIColor.jamiBackgroundColor noConversationLabel.textColor = UIColor.jamiLabelColor - searchTableViewLabel.textColor = UIColor.jamiLabelColor dialpadButtonShadow.backgroundColor = UIColor.jamiBackgroundSecondaryColor dialpadButtonShadow.layer.shadowColor = UIColor.jamiLabelColor.cgColor dialpadButtonShadow.layer.shadowOffset = CGSize.zero @@ -153,7 +154,7 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased tableTopConstraint.constant = !isHidden ? -(SmartlistConstants.tableViewOffset - SmartlistConstants.networkAllerHeight) : -SmartlistConstants.tableViewOffset self.networkAlertView.isHidden = isHidden self.viewModel.connectionState - .subscribe(onNext: { connectionState in + .subscribe(onNext: { [weak self] connectionState in let newAlertHeight = connectionState == .none ? 0.0 : -SmartlistConstants.networkAllerHeight let newTableViewTop = connectionState == .none ? -(SmartlistConstants.tableViewOffset - SmartlistConstants.networkAllerHeight) : -SmartlistConstants.tableViewOffset let isHidden = connectionState == .none ? false : true @@ -162,7 +163,7 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased self?.tableTopConstraint.constant = CGFloat(newTableViewTop) self?.view.layoutIfNeeded() } - self.networkAlertView.isHidden = isHidden + self?.networkAlertView.isHidden = isHidden }) .disposed(by: self.disposeBag) @@ -257,7 +258,6 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased }) .disposed(by: self.disposeBag) self.conversationsTableView.tableFooterView = UIView() - self.searchResultsTableView.tableFooterView = UIView() } func confugureAccountPicker() { @@ -275,7 +275,8 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased } self.viewModel.currentAccountChanged .observeOn(MainScheduler.instance) - .subscribe(onNext: { [unowned self] currentAccount in + .subscribe(onNext: { [weak self] currentAccount in + guard let self = self else { return } if let account = currentAccount, let row = self.accountsAdapter.rowForAccountId(account: account) { self.accounPicker.selectRow(row, inComponent: 0, animated: true) @@ -325,17 +326,17 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased } self.conversationsTableView.contentInset.bottom = keyboardHeight - tabBarHeight - self.searchResultsTableView.contentInset.bottom = keyboardHeight - tabBarHeight + self.searchView.searchResultsTableView.contentInset.bottom = keyboardHeight - tabBarHeight self.conversationsTableView.scrollIndicatorInsets.bottom = keyboardHeight - tabBarHeight - self.searchResultsTableView.scrollIndicatorInsets.bottom = keyboardHeight - tabBarHeight + self.searchView.searchResultsTableView.scrollIndicatorInsets.bottom = keyboardHeight - tabBarHeight } @objc func keyboardWillHide(withNotification notification: Notification) { self.conversationsTableView.contentInset.bottom = 0 - self.searchResultsTableView.contentInset.bottom = 0 + self.searchView.searchResultsTableView.contentInset.bottom = 0 self.conversationsTableView.scrollIndicatorInsets.bottom = 0 - self.searchResultsTableView.scrollIndicatorInsets.bottom = 0 + self.searchView.searchResultsTableView.scrollIndicatorInsets.bottom = 0 } func setupDataSources() { @@ -347,15 +348,13 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased indexPath: IndexPath, conversationItem: ConversationSection.Item) in - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ConversationCell.self) + let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SmartListCell.self) cell.configureFromItem(conversationItem) return cell } //Create DataSources for conversations and filtered conversations let conversationsDataSource = RxTableViewSectionedReloadDataSource<ConversationSection>(configureCell: configureCell) - let searchResultsDatasource = RxTableViewSectionedReloadDataSource<ConversationSection>(configureCell: configureCell) - //Allows to delete conversationsDataSource.canEditRowAtIndexPath = { _, _ in return true @@ -365,58 +364,24 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased self.viewModel.conversations .bind(to: self.conversationsTableView.rx.items(dataSource: conversationsDataSource)) .disposed(by: disposeBag) - - self.viewModel.searchResults - .bind(to: self.searchResultsTableView.rx.items(dataSource: searchResultsDatasource)) - .disposed(by: disposeBag) - - //Set header titles - searchResultsDatasource.titleForHeaderInSection = { dataSource, index in - return dataSource.sectionModels[index].header - } } - func setupTableViews() { + func setupTableView() { //Set row height 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: { [unowned self] (isSearching) in - self.searchResultsTableView.isHidden = !isSearching - self.searchTableViewLabel.isHidden = !isSearching - }).disposed(by: disposeBag) - + self.conversationsTableView.register(cellType: SmartListCell.self) //Deselect the rows self.conversationsTableView.rx.itemSelected.subscribe(onNext: { [unowned self] indexPath in self.conversationsTableView.deselectRow(at: indexPath, animated: true) }).disposed(by: disposeBag) - self.searchResultsTableView.rx.itemSelected.subscribe(onNext: { [unowned self] indexPath in - self.searchResultsTableView.deselectRow(at: indexPath, animated: true) - }).disposed(by: disposeBag) - - //Bind the search status label - self.viewModel.searchStatus - .observeOn(MainScheduler.instance) - .bind(to: self.searchTableViewLabel.rx.text) - .disposed(by: disposeBag) - - self.searchResultsTableView.rx.setDelegate(self).disposed(by: disposeBag) self.conversationsTableView.rx.setDelegate(self).disposed(by: disposeBag) } func setupSearchBar() { - self.searchBar.returnKeyType = .done - self.searchBar.autocapitalizationType = .none - self.searchBar.tintColor = UIColor.jamiMain - searchBar.backgroundImage = UIImage() searchBarShadow.backgroundColor = UIColor.clear - searchBar.backgroundColor = UIColor.clear self.searchBarShadow.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor self.searchBarShadow.layer.shadowOffset = CGSize(width: 0.0, height: 1.5) self.searchBarShadow.layer.shadowOpacity = 0.2 @@ -458,40 +423,11 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased visualEffectView.bottomAnchor.constraint(equalTo: self.searchBarShadow.bottomAnchor, constant: 0).isActive = true background.bottomAnchor.constraint(equalTo: self.searchBarShadow.bottomAnchor, constant: 0).isActive = true } - - //Bind the SearchBar to the ViewModel - self.searchBar.rx.text.orEmpty - .debounce(Durations.textFieldThrottlingDuration.value, scheduler: MainScheduler.instance) - .bind(to: self.viewModel.searchBarText) - .disposed(by: disposeBag) - - //Show Cancel button - self.searchBar.rx.textDidBeginEditing.subscribe(onNext: { [unowned self] in - self.scanButtonLeadingConstraint.constant = -40 - self.searchBar.setShowsCancelButton(true, animated: false) - }).disposed(by: disposeBag) - - //Hide Cancel button - self.searchBar.rx.textDidEndEditing.subscribe(onNext: { [unowned self] in - self.scanButtonLeadingConstraint.constant = 10 - self.searchBar.setShowsCancelButton(false, animated: false) + self.searchView.editSearch + .subscribe(onNext: {[weak self] (editing) in + self?.scanButtonLeadingConstraint.constant = editing ? -40 : 10 + self?.viewModel.searching.onNext(editing) }).disposed(by: disposeBag) - - //Cancel button event - self.searchBar.rx.cancelButtonClicked.subscribe(onNext: { [unowned self] in - self.cancelSearch() - }).disposed(by: disposeBag) - - //Search button event - self.searchBar.rx.searchButtonClicked.subscribe(onNext: { [unowned self] in - self.searchBar.resignFirstResponder() - }).disposed(by: disposeBag) - } - - func cancelSearch() { - self.searchBar.text = "" - self.searchBar.resignFirstResponder() - self.searchResultsTableView.isHidden = true } func startAccountCreation() { @@ -500,7 +436,7 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased } func openAccountsList() { - if searchBar.isFirstResponder { + if searchView.searchBar.isFirstResponder { return } if accountPickerTextView.isFirstResponder { diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift index f25fd8412543166a2fb16fdc9b352c878518e680..8a1cd465ee536588c632a0737f3ec2076854e5a1 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift @@ -24,7 +24,7 @@ import RxSwift import SwiftyBeaver -class SmartlistViewModel: Stateable, ViewModel { +class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { private let log = SwiftyBeaver.self @@ -46,33 +46,21 @@ class SmartlistViewModel: Stateable, ViewModel { fileprivate let profileService: ProfilesService fileprivate let callService: CallsService - let searchBarText = Variable<String>("") - var isSearching: Observable<Bool>! lazy var currentAccount: AccountModel? = { return self.accountsService.currentAccount }() - lazy var searchResults: Observable<[ConversationSection]> = { - return Observable<[ConversationSection]> - .combineLatest(self.contactFoundConversation - .asObservable(), - self.filteredResults.asObservable(), - resultSelector: { contactFoundConversation, filteredResults in - - var sections = [ConversationSection]() - if !filteredResults.isEmpty { - sections.append(ConversationSection(header: L10n.Smartlist.conversations, items: filteredResults)) - } else if contactFoundConversation != nil { - sections.append(ConversationSection(header: L10n.Smartlist.results, items: [contactFoundConversation!])) - } - return sections - }).observeOn(MainScheduler.instance) - }() + var searching = PublishSubject<Bool>() + + fileprivate var contactFoundConversation = Variable<ConversationViewModel?>(nil) + lazy var hideNoConversationsMessage: Observable<Bool> = { return Observable<Bool> - .combineLatest(self.conversations, self.searchBarText.asObservable(), - resultSelector: {(conversations, searchBarText) -> Bool in - if !searchBarText.isEmpty {return true} + .combineLatest(self.conversations, + self.searching.asObservable() + .startWith(false), + resultSelector: {(conversations, searching) -> Bool in + if searching {return true} if let convf = conversations.first { return !convf.items.isEmpty } @@ -80,13 +68,13 @@ class SmartlistViewModel: Stateable, ViewModel { }).observeOn(MainScheduler.instance) }() - var searchStatus = PublishSubject<String>() var connectionState = PublishSubject<ConnectionType>() lazy var accounts: Observable<[AccountItem]> = { return self.accountsService .accountsObservable.asObservable() - .map({ accountsModels in + .map({ [weak self] accountsModels in var items = [AccountItem]() + guard let self = self else { return items } for account in accountsModels { items.append(AccountItem(account: account, profileObservable: self.profileService.getAccountProfile(accountId: account.id))) @@ -95,8 +83,6 @@ class SmartlistViewModel: Stateable, ViewModel { }) }() - fileprivate var filteredResults = Variable([ConversationViewModel]()) - fileprivate var contactFoundConversation = Variable<ConversationViewModel?>(nil) var conversationViewModels = [ConversationViewModel]() func networkConnectionState() -> ConnectionType { @@ -232,102 +218,10 @@ class SmartlistViewModel: Stateable, ViewModel { self.connectionState.onNext(value) }) .disposed(by: self.disposeBag) - - //Observes if the user is searching - self.isSearching = searchBarText.asObservable() - .map({ text in - return !text.isEmpty - }).observeOn(MainScheduler.instance) - - //Observes search bar text - searchBarText.asObservable() - .observeOn(MainScheduler.instance) - .subscribe(onNext: { [unowned self] text in - self.search(withText: text) - }).disposed(by: disposeBag) - - //Observe username lookup - self.nameService.usernameLookupStatus - .observeOn(MainScheduler.instance) - .subscribe(onNext: { [unowned self, unowned injectionBag] usernameLookupStatus in - if usernameLookupStatus.state == .found && - (usernameLookupStatus.name == self.searchBarText.value - || usernameLookupStatus.address == self.searchBarText.value) { - if let conversation = self.conversationViewModels.filter({ conversationViewModel in - conversationViewModel.conversation.value.participantUri == usernameLookupStatus.address || conversationViewModel.conversation.value.hash == usernameLookupStatus.address - }).first { - self.contactFoundConversation.value = conversation - } else { - if self.contactFoundConversation.value?.conversation.value - .participantUri != usernameLookupStatus.address && self.contactFoundConversation.value?.conversation.value - .hash != usernameLookupStatus.address { - if let account = self.accountsService.currentAccount { - let uri = JamiURI.init(schema: URIType.ring, infoHach: usernameLookupStatus.address) - //Create new converation - let conversation = ConversationModel(withParticipantUri: uri, accountId: account.id) - let newConversation = ConversationViewModel(with: injectionBag) - newConversation.conversation = Variable<ConversationModel>(conversation) - self.contactFoundConversation.value = newConversation - } - } - } - self.searchStatus.onNext("") - } else { - if self.filteredResults.value.isEmpty - && self.contactFoundConversation.value == nil { - self.searchStatus.onNext(L10n.Smartlist.noResults) - } else { - self.searchStatus.onNext("") - } - } - }).disposed(by: disposeBag) } - fileprivate func search(withText text: String) { - guard let currentAccount = self.accountsService.currentAccount else { return } - - self.contactFoundConversation.value = nil - self.filteredResults.value.removeAll() - self.searchStatus.onNext("") - - if text.isEmpty {return} - - //Filter conversations - let filteredConversations = self.conversationViewModels - .filter({conversationViewModel in - conversationViewModel.conversation.value.participantUri == text - || conversationViewModel.conversation.value.hash == text - }) - - if !filteredConversations.isEmpty { - self.filteredResults.value = filteredConversations - } - - if currentAccount.type == AccountType.sip { - let uri = JamiURI.init(schema: URIType.sip, infoHach: text, account: currentAccount) - let conversation = ConversationModel(withParticipantUri: uri, - accountId: currentAccount.id, - hash: text) - let newConversation = ConversationViewModel(with: self.injectionBag) - newConversation.conversation = Variable<ConversationModel>(conversation) - self.contactFoundConversation.value = newConversation - return - } - - if !text.isSHA1() { - self.nameService.lookupName(withAccount: currentAccount.id, nameserver: "", name: text) - self.searchStatus.onNext(L10n.Smartlist.searching) - return - } - - if self.contactFoundConversation.value?.conversation.value.participantUri != text && self.contactFoundConversation.value?.conversation.value.hash != text { - let uri = JamiURI.init(schema: URIType.ring, infoHach: text) - let conversation = ConversationModel(withParticipantUri: uri, - accountId: currentAccount.id) - let newConversation = ConversationViewModel(with: self.injectionBag) - newConversation.conversation = Variable<ConversationModel>(conversation) - self.contactFoundConversation.value = newConversation - } + func conversationFound(conversation: ConversationViewModel?, name: String) { + contactFoundConversation.value = conversation } func delete(conversationViewModel: ConversationViewModel) { diff --git a/Ring/Ring/Features/Conversations/views/ConfirmationAlert.swift b/Ring/Ring/Features/Conversations/views/ConfirmationAlert.swift new file mode 100644 index 0000000000000000000000000000000000000000..dd228efebadadcbf5a6fe27577986f353178d2ea --- /dev/null +++ b/Ring/Ring/Features/Conversations/views/ConfirmationAlert.swift @@ -0,0 +1,97 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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 + +protocol BoothModeConfirmationPresenter: UIViewController { + func showLoadingViewWithoutText() + func stopLoadingView() + func enableBoothMode(enable: Bool, password: String) -> Bool + func switchBoothModeState(state: Bool) +} + +class ConfirmationAlert { + var alert = UIAlertController() + func configure(title: String, + msg: String, + enable: Bool, + presenter: BoothModeConfirmationPresenter, + disposeBag: DisposeBag) { + alert = UIAlertController(title: title, + message: msg, + preferredStyle: .alert) + let actionCancel = UIAlertAction(title: L10n.Actions.cancelAction, + style: .cancel) { [weak presenter] _ in + presenter?.switchBoothModeState(state: !enable) + } + let actionConfirm = UIAlertAction(title: L10n.Actions.doneAction, + style: .default) { [weak presenter, weak self] _ in + presenter?.showLoadingViewWithoutText() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + presenter?.stopLoadingView() + if let textFields = self?.alert.textFields, + !textFields.isEmpty, + let text = textFields[0].text, + !text.isEmpty { + let result = presenter?.enableBoothMode(enable: enable, password: text) + if result ?? true { + return + } + presenter?.switchBoothModeState(state: !enable) + guard let self = self else { + return + } + presenter?.present(self.alert, animated: true, completion: nil) + textFields[1].text = L10n.AccountPage.changePasswordError + textFields[1].textColor = UIColor.red + } + } + } + alert.addAction(actionCancel) + alert.addAction(actionConfirm) + alert.addTextField {(textField) in + textField.placeholder = L10n.Account.passwordLabel + textField.isSecureTextEntry = true + } + alert.addTextField {(textField) in + textField.text = "" + textField.isUserInteractionEnabled = false + textField.textColor = UIColor.jamiLabelColor + textField.textAlignment = .center + textField.borderStyle = .none + textField.backgroundColor = UIColor.clear + textField.font = UIFont.systemFont(ofSize: 11, weight: .thin) + textField.text = L10n.AccountPage.passwordPlaceholder + } + + if let textFields = alert.textFields { + textFields[0].rx.text.map({text in + if let text = text { + return !text.isEmpty + } + return false + }).bind(to: actionConfirm.rx.isEnabled) + .disposed(by: disposeBag) + } + presenter.present(alert, animated: true, completion: nil) + alert.textFields?[1].superview?.backgroundColor = .clear + alert.textFields?[1].superview?.superview?.subviews[0].removeFromSuperview() + } +} diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift new file mode 100644 index 0000000000000000000000000000000000000000..d43c3e57685776428d387e8b1cbdfe0457fcdcd6 --- /dev/null +++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift @@ -0,0 +1,172 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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 RxDataSources +import RxCocoa +import Reusable + +class JamiSearchView: NSObject, UITableViewDelegate { + @IBOutlet weak var searchBar: UISearchBar! + @IBOutlet weak var searchingLabel: UILabel! + @IBOutlet weak var searchResultsTableView: UITableView! + + fileprivate var viewModel: JamiSearchViewModel! + fileprivate let disposeBag = DisposeBag() + var editSearch = PublishSubject<Bool>() + var isIncognito = false + + let incognitoCellHeight: CGFloat = 150 + let incognitoHeaderHeight: CGFloat = 0 + + func configure(with injectionBag: InjectionBag, source: FilterConversationDataSource, isIncognito: Bool) { + viewModel = JamiSearchViewModel(with: injectionBag, source: source) + self.isIncognito = isIncognito + setUpView() + } + + private func setUpView() { + configureSearchResult() + configureSearchBar() + } + private func cancelSearch() { + self.searchBar.text = "" + self.searchBar.resignFirstResponder() + self.searchResultsTableView.isHidden = true + } + + private func configureSearchResult() { + let cellType = isIncognito ? IncognitoSmartListCell.self : SmartListCell.self + searchResultsTableView.register(cellType: cellType) + + searchResultsTableView.rowHeight = isIncognito ? incognitoCellHeight : SmartlistConstants.smartlistRowHeight + searchResultsTableView.backgroundColor = UIColor.jamiBackgroundColor + if !isIncognito { + searchResultsTableView.tableFooterView = UIView() + } + searchResultsTableView.rx.setDelegate(self).disposed(by: disposeBag) + let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, ConversationSection.Item) + -> UITableViewCell = { + ( dataSource: TableViewSectionedDataSource<ConversationSection>, + tableView: UITableView, + indexPath: IndexPath, + conversationItem: ConversationSection.Item) in + + let cell = self.isIncognito ? + tableView.dequeueReusableCell(for: indexPath, + cellType: IncognitoSmartListCell.self) : + tableView.dequeueReusableCell(for: indexPath, + cellType: SmartListCell.self) + cell.configureFromItem(conversationItem) + return cell + } + let searchResultsDatasource = RxTableViewSectionedReloadDataSource<ConversationSection>(configureCell: configureCell) + viewModel.searchResults.map { (conversations) -> Bool in + return conversations.isEmpty + }.subscribe(onNext: { [weak self] (hideFooterView) in + self?.searchResultsTableView.tableFooterView?.isHidden = hideFooterView + }).disposed(by: disposeBag) + + self.viewModel.searchResults + .bind(to: self.searchResultsTableView.rx.items(dataSource: searchResultsDatasource)) + .disposed(by: disposeBag) + searchResultsTableView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath in + self?.searchResultsTableView.deselectRow(at: indexPath, animated: true) + }).disposed(by: disposeBag) + searchResultsDatasource.titleForHeaderInSection = { dataSource, index in + return dataSource.sectionModels[index].header + } + //search status label + self.viewModel.searchStatus + .observeOn(MainScheduler.instance) + .bind(to: self.searchingLabel.rx.text) + .disposed(by: disposeBag) + searchingLabel.textColor = UIColor.jamiLabelColor + + self.viewModel.isSearching.subscribe(onNext: { [weak self] (isSearching) in + self?.searchResultsTableView.isHidden = !isSearching + self?.searchingLabel.isHidden = !isSearching + }).disposed(by: disposeBag) + } + + private func configureSearchBar() { + self.searchBar.rx.text.orEmpty + .debounce(Durations.textFieldThrottlingDuration.value, scheduler: MainScheduler.instance) + .bind(to: self.viewModel.searchBarText) + .disposed(by: disposeBag) + + //Show Cancel button + self.searchBar.rx.textDidBeginEditing + .subscribe(onNext: { [weak self] in + self?.editSearch.onNext(true) + self?.searchBar.setShowsCancelButton(true, animated: false) + }).disposed(by: disposeBag) + + //Hide Cancel button + self.searchBar.rx.textDidEndEditing + .subscribe(onNext: { [weak self] in + guard let self = self else { return } + self.searchBar.setShowsCancelButton(false, animated: false) + if self.isIncognito && !(self.searchBar.text?.isEmpty ?? true) { + return + } + self.editSearch.onNext(false) + }).disposed(by: disposeBag) + + //Cancel button event + self.searchBar.rx.cancelButtonClicked + .subscribe(onNext: { [weak self] in + self?.cancelSearch() + }).disposed(by: disposeBag) + + //Search button event + self.searchBar.rx.searchButtonClicked + .subscribe(onNext: { [weak self] in + self?.searchBar.resignFirstResponder() + }).disposed(by: disposeBag) + + searchBar.returnKeyType = .done + searchBar.autocapitalizationType = .none + searchBar.tintColor = UIColor.jamiMain + searchBar.placeholder = L10n.Smartlist.searchBarPlaceholder + searchBar.searchBarStyle = .minimal + searchBar.backgroundImage = UIImage() + searchBar.placeholder = L10n.Smartlist.searchBarPlaceholder + searchBar.backgroundColor = UIColor.clear + } + +// MARK: UITableViewDelegate + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + guard let headerView = view as? UITableViewHeaderFooterView else { return } + headerView.tintColor = .clear + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return isIncognito ? incognitoHeaderHeight : SmartlistConstants.tableHeaderViewHeight + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let convToShow: ConversationViewModel = try? tableView.rx.model(at: indexPath) { + self.viewModel.showConversation(conversation: convToShow) + } + } +} diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..74493b3530d19cc986dd292fa280b03dd2fa8697 --- /dev/null +++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift @@ -0,0 +1,176 @@ +/* +* Copyright (C) 2020 Savoir-faire Linux Inc. +* +* Author: Kateryna Kostiuk <kateryna.kostiuk@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 +import RxCocoa + +protocol FilterConversationDataSource { + var conversationViewModels: [ConversationViewModel] { get set } + func conversationFound(conversation: ConversationViewModel?, name: String) + func showConversation(withConversationViewModel conversationViewModel: ConversationViewModel) +} + +class JamiSearchViewModel { + + //Services + fileprivate let nameService: NameService + fileprivate let accountsService: AccountsService + fileprivate let injectionBag: InjectionBag + + fileprivate let disposeBag = DisposeBag() + + lazy var searchResults: Observable<[ConversationSection]> = { + return Observable<[ConversationSection]> + .combineLatest(self.contactFoundConversation + .asObservable(), + self.filteredResults.asObservable(), + resultSelector: { contactFoundConversation, filteredResults in + var sections = [ConversationSection]() + if !filteredResults.isEmpty { + sections.append(ConversationSection(header: L10n.Smartlist.conversations, items: filteredResults)) + } else if contactFoundConversation != nil { + sections.append(ConversationSection(header: L10n.Smartlist.results, items: [contactFoundConversation!])) + } + return sections + }).observeOn(MainScheduler.instance) + }() + + fileprivate var contactFoundConversation = BehaviorRelay<ConversationViewModel?>(value: nil) + fileprivate var filteredResults = Variable([ConversationViewModel]()) + + let searchBarText = Variable<String>("") + var isSearching: Observable<Bool>! + var searchStatus = PublishSubject<String>() + let dataSource: FilterConversationDataSource + + init(with injectionBag: InjectionBag, source: FilterConversationDataSource) { + self.nameService = injectionBag.nameService + self.accountsService = injectionBag.accountService + self.injectionBag = injectionBag + dataSource = source + + //Observes if the user is searching + self.isSearching = searchBarText.asObservable() + .map({ text in + return !text.isEmpty + }).observeOn(MainScheduler.instance) + + //Observes search bar text + searchBarText.asObservable() + .observeOn(MainScheduler.instance) + .distinctUntilChanged() + .subscribe(onNext: { [weak self] text in + self?.search(withText: text) + }).disposed(by: disposeBag) + + //Observe username lookup + self.nameService.usernameLookupStatus + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [unowned self, unowned injectionBag] usernameLookupStatus in + if usernameLookupStatus.state == .found && + (usernameLookupStatus.name == self.searchBarText.value + || usernameLookupStatus.address == self.searchBarText.value) { + if let conversation = self.dataSource.conversationViewModels.filter({ conversationViewModel in + conversationViewModel.conversation.value.participantUri == usernameLookupStatus.address || conversationViewModel.conversation.value.hash == usernameLookupStatus.address + }).first { + self.contactFoundConversation.accept(conversation) + self.dataSource.conversationFound(conversation: conversation, name: self.searchBarText.value) + } else { + if self.contactFoundConversation.value?.conversation.value + .participantUri != usernameLookupStatus.address && self.contactFoundConversation.value?.conversation.value + .hash != usernameLookupStatus.address { + if let account = self.accountsService.currentAccount { + let uri = JamiURI.init(schema: URIType.ring, infoHach: usernameLookupStatus.address) + //Create new converation + let conversation = ConversationModel(withParticipantUri: uri, accountId: account.id) + let newConversation = ConversationViewModel(with: injectionBag) + newConversation.conversation = Variable<ConversationModel>(conversation) + self.contactFoundConversation.accept(newConversation) + self.dataSource.conversationFound(conversation: newConversation, name: self.searchBarText.value) + } + } + } + self.searchStatus.onNext("") + } else { + if self.filteredResults.value.isEmpty + && self.contactFoundConversation.value == nil { + self.searchStatus.onNext(L10n.Smartlist.noResults) + } else { + self.searchStatus.onNext("") + } + } + }).disposed(by: disposeBag) + } + + fileprivate func search(withText text: String) { + guard let currentAccount = self.accountsService.currentAccount else { return } + + self.contactFoundConversation.accept(nil) + self.dataSource.conversationFound(conversation: nil, name: "") + self.filteredResults.value.removeAll() + self.searchStatus.onNext("") + + if text.isEmpty {return} + + //Filter conversations + let filteredConversations = self.dataSource.conversationViewModels + .filter({conversationViewModel in + conversationViewModel.conversation.value.participantUri == text + || conversationViewModel.conversation.value.hash == text + }) + + if !filteredConversations.isEmpty { + self.filteredResults.value = filteredConversations + } + + if currentAccount.type == AccountType.sip { + let uri = JamiURI.init(schema: URIType.sip, infoHach: text, account: currentAccount) + let conversation = ConversationModel(withParticipantUri: uri, + accountId: currentAccount.id, + hash: text) + let newConversation = ConversationViewModel(with: self.injectionBag) + newConversation.conversation = Variable<ConversationModel>(conversation) + self.contactFoundConversation.accept(newConversation) + self.dataSource.conversationFound(conversation: newConversation, name: self.searchBarText.value) + return + } + + if !text.isSHA1() { + self.nameService.lookupName(withAccount: currentAccount.id, nameserver: "", name: text) + self.searchStatus.onNext(L10n.Smartlist.searching) + return + } + + if self.contactFoundConversation.value?.conversation.value.participantUri != text && self.contactFoundConversation.value?.conversation.value.hash != text { + let uri = JamiURI.init(schema: URIType.ring, infoHach: text) + let conversation = ConversationModel(withParticipantUri: uri, + accountId: currentAccount.id) + let newConversation = ConversationViewModel(with: self.injectionBag) + newConversation.conversation = Variable<ConversationModel>(conversation) + self.contactFoundConversation.accept(newConversation) + self.dataSource.conversationFound(conversation: newConversation, name: self.searchBarText.value) + } + } + + func showConversation(conversation: ConversationViewModel) { + dataSource.showConversation(withConversationViewModel: conversation) + } +} diff --git a/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift b/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift index e4e217bd6b290e6757bcf78055757b3424d78cd2..88f76c4e7409647136d6ca451e87ae7c1246c4b8 100644 --- a/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift +++ b/Ring/Ring/Features/Me/LinkNewDeviceViewModel.swift @@ -86,7 +86,7 @@ class LinkNewDeviceViewModel: ViewModel, Stateable { lazy var hasPassord: Bool = { guard let currentAccount = self.accountService.currentAccount else {return true} - return AccountModelHelper(withAccount: currentAccount).havePassword + return AccountModelHelper(withAccount: currentAccount).hasPassword }() let accountService: AccountsService diff --git a/Ring/Ring/Features/Me/Me/MeViewController.swift b/Ring/Ring/Features/Me/Me/MeViewController.swift index fed5ae63f7d24cd4c7d48a239c06b7bcec4d3560..1719a21ed501d931b06f4472f9ae4672815566ef 100644 --- a/Ring/Ring/Features/Me/Me/MeViewController.swift +++ b/Ring/Ring/Features/Me/Me/MeViewController.swift @@ -174,10 +174,6 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas self.viewModel.showBlockedContacts() } - private func stopLoadingView() { - HUD.hide(animated: false) - } - private func showLoadingView() { HUD.show(.labeledProgress(title: L10n.AccountPage.deviceRevocationProgress, subtitle: nil)) } @@ -421,7 +417,24 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas self?.shareAccountInfo() }).disposed(by: cell.disposeBag) return cell - + case .changePassword: + let cell = DisposableCell() + cell.backgroundColor = UIColor.jamiBackgroundColor + let title = self.viewModel.hasPassword() ? + L10n.AccountPage.changePassword : L10n.AccountPage.createPassword + cell.textLabel?.text = title + cell.textLabel?.textColor = UIColor.jamiMain + cell.textLabel?.textAlignment = .center + cell.sizeToFit() + cell.selectionStyle = .none + let button = UIButton.init(frame: cell.frame) + let size = CGSize(width: self.view.frame.width, height: button.frame.height) + button.frame.size = size + cell.addSubview(button) + button.rx.tap.subscribe(onNext: { [weak self] in + self?.changePassword(title: title) + }).disposed(by: cell.disposeBag) + return cell case .notifications: let cell = DisposableCell() cell.backgroundColor = UIColor.jamiBackgroundColor @@ -483,6 +496,40 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas cell.detailTextLabel?.text = status }).disposed(by: cell.disposeBag) return cell + case .boothMode: + let cell = DisposableCell(style: .subtitle, reuseIdentifier: self.jamiIDCell) + cell.backgroundColor = UIColor.jamiBackgroundColor + cell.textLabel?.text = L10n.AccountPage.enableBoothMode + cell.textLabel?.sizeToFit() + let switchView = UISwitch() + cell.selectionStyle = .none + cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator + cell.accessoryView = switchView + cell.detailTextLabel?.text = self.viewModel.hasPassword() ? + L10n.AccountPage.boothModeExplanation : L10n.AccountPage.noBoothMode + cell.detailTextLabel?.lineBreakMode = .byWordWrapping + cell.detailTextLabel?.numberOfLines = 0 + cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) + cell.sizeToFit() + cell.layoutIfNeeded() + self.viewModel.switchBoothModeState + .observeOn(MainScheduler.instance) + .bind(to: switchView.rx.value) + .disposed(by: self.disposeBag) + switchView.rx + .isOn.changed + .subscribe(onNext: {[weak self] enable in + if !enable { + return + } + self?.viewModel.switchBoothModeState.onNext(enable) + self?.confirmBoothModeAlert() + }).disposed(by: self.disposeBag) + cell.isUserInteractionEnabled = self.viewModel.hasPassword() + cell.textLabel?.isEnabled = self.viewModel.hasPassword() + cell.detailTextLabel?.isEnabled = self.viewModel.hasPassword() + switchView.isEnabled = self.viewModel.hasPassword() + return cell case .enableAccount: let cell = DisposableCell() cell.backgroundColor = UIColor.jamiBackgroundColor @@ -695,6 +742,100 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas self.viewModel.updateSipSettings() } + let boothConfirmation = ConfirmationAlert() + + func confirmBoothModeAlert() { + boothConfirmation.configure(title: L10n.AccountPage.enableBoothMode, + msg: L10n.AccountPage.boothModeAlertMessage, + enable: true, presenter: self, + disposeBag: self.disposeBag) + } + + func changePassword(title: String) { + let controller = UIAlertController(title: title, + message: nil, + preferredStyle: .alert) + let actionCancel = UIAlertAction(title: L10n.Actions.cancelAction, + style: .cancel) + let actionChange = UIAlertAction(title: L10n.Actions.doneAction, + style: .default) { [weak self] _ in + guard let textFields = controller.textFields else { + return + } + self?.showLoadingViewWithoutText() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + if textFields.count == 2, let password = textFields[1].text { + _ = self?.viewModel + .changePassword(oldPassword: "", + newPassword: password) + self?.stopLoadingView() + } else if textFields.count == 4, + let oldPassword = textFields[0].text, !oldPassword.isEmpty, + let password = textFields[2].text { + let result = self?.viewModel.changePassword(oldPassword: oldPassword, newPassword: password) + if result ?? true { + self?.stopLoadingView() + return + } + self?.present(controller, animated: true, completion: nil) + textFields[1].text = L10n.AccountPage.changePasswordError + self?.stopLoadingView() + } + } + } + controller.addAction(actionCancel) + controller.addAction(actionChange) + if self.viewModel.hasPassword() { + controller.addTextField {(textField) in + textField.placeholder = L10n.AccountPage.oldPasswordPlaceholder + textField.isSecureTextEntry = true + } + controller.addTextField {(textField) in + textField.text = "" + textField.isUserInteractionEnabled = false + textField.textColor = UIColor.red + textField.textAlignment = .center + textField.borderStyle = .none + textField.backgroundColor = UIColor.clear + textField.font = UIFont.systemFont(ofSize: 11, weight: .thin) + } + } + + controller.addTextField {(textField) in + textField.placeholder = L10n.AccountPage.newPasswordPlaceholder + textField.isSecureTextEntry = true + } + + controller.addTextField {(textField) in + textField.placeholder = L10n.AccountPage.newPasswordConfirmPlaceholder + textField.isSecureTextEntry = true + } + + if let textFields = controller.textFields { + if textFields.count == 4 { + Observable + .combineLatest(textFields[3].rx.text, + textFields[2].rx.text) {(text1, text2) -> Bool in + return text1 == text2 } + .bind(to: actionChange.rx.isEnabled) + .disposed(by: self.disposeBag) + } else { + Observable + .combineLatest(textFields[0].rx.text, + textFields[1].rx.text) {(text1, text2) -> Bool in + return text1 == text2 } + .bind(to: actionChange.rx.isEnabled) + .disposed(by: self.disposeBag) + } + } + self.present(controller, animated: true, completion: nil) + if self.viewModel.hasPassword() { + //remove border around text view + controller.textFields?[1].superview?.backgroundColor = .clear + controller.textFields?[1].superview?.superview?.subviews[0].removeFromSuperview() + } + } + var nameRegistrationBag = DisposeBag() func registerUsername() { @@ -740,7 +881,7 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas textField.font = UIFont.systemFont(ofSize: 11, weight: .thin) } //password text field - if self.viewModel.havePassord() { + if self.viewModel.hasPassword() { controller.addTextField {(textField) in textField.placeholder = L10n.AccountPage.passwordPlaceholder textField.isSecureTextEntry = true @@ -835,7 +976,7 @@ class MeViewController: EditProfileViewController, StoryboardBased, ViewModelBas alert.addAction(actionCancel) alert.addAction(actionConfirm) - if self.viewModel.havePassord() { + if self.viewModel.hasPassword() { alert.addTextField {(textField) in textField.placeholder = L10n.AccountPage.revokeDevicePlaceholder textField.isSecureTextEntry = true @@ -930,3 +1071,21 @@ extension MeViewController: UITableViewDelegate { self.settingsTable.setContentOffset(contentOffset, animated: true) } } + +extension MeViewController: BoothModeConfirmationPresenter { + func enableBoothMode(enable: Bool, password: String) -> Bool { + return self.viewModel.enableBoothMode(enable: enable, password: password) + } + + func switchBoothModeState(state: Bool) { + self.viewModel.switchBoothModeState.onNext(state) + } + + internal func stopLoadingView() { + HUD.hide(animated: false) + } + + internal func showLoadingViewWithoutText() { + HUD.show(.labeledProgress(title: "", subtitle: nil)) + } +} diff --git a/Ring/Ring/Features/Me/Me/MeViewModel.swift b/Ring/Ring/Features/Me/Me/MeViewModel.swift index fdea56e2ca178864c6ba718d401436e2816cf7fc..dec3a49ca65b1f893e9cf4eb6700fe88b919d9d0 100644 --- a/Ring/Ring/Features/Me/Me/MeViewModel.swift +++ b/Ring/Ring/Features/Me/Me/MeViewModel.swift @@ -53,6 +53,8 @@ enum SettingsSection: SectionModelType { case proxyServer(value: String) case accountState(state: Variable<String>) case enableAccount + case changePassword + case boothMode } var items: [SectionRow] { @@ -105,6 +107,8 @@ class MeViewModel: ViewModel, Stateable { let accountService: AccountsService let nameService: NameService + let contactService: ContactsService + let presenceService: PresenceService // MARK: - configure table sections @@ -211,12 +215,14 @@ class MeViewModel: ViewModel, Stateable { .blockedList, .accountState(state: self.accountStatus), .enableAccount, + .changePassword, + .boothMode, .removeAccount])) }() - func havePassord() -> Bool { + func hasPassword() -> Bool { guard let currentAccount = self.accountService.currentAccount else {return true} - return AccountModelHelper(withAccount: currentAccount).havePassword + return AccountModelHelper(withAccount: currentAccount).hasPassword } lazy var jamiSettings: Observable<[SettingsSection]> = { @@ -308,6 +314,8 @@ class MeViewModel: ViewModel, Stateable { required init (with injectionBag: InjectionBag) { self.accountService = injectionBag.accountService self.nameService = injectionBag.nameService + self.contactService = injectionBag.contactsService + self.presenceService = injectionBag.presenceService self.secureTextEntry.onNext(true) } @@ -436,6 +444,30 @@ class MeViewModel: ViewModel, Stateable { }).disposed(by: self.disposeBag) } + func changePassword(oldPassword: String, newPassword: String) -> Bool { + guard let accountId = self.accountService.currentAccount?.id else { + return false + } + return self.accountService + .changePassword(forAccount: accountId, password: oldPassword, newPassword: newPassword) + } + + var switchBoothModeState = PublishSubject<Bool>() + + func enableBoothMode(enable: Bool, password: String) -> Bool { + guard let accountId = self.accountService.currentAccount?.id else { + return false + } + let result = self.accountService.setBoothMode(forAccount: accountId, enable: enable, password: password) + if !result { + return false + } + self.stateSubject.onNext(MeState.accountModeChanged) + self.presenceService.subscribeBuddies(withAccount: accountId, withContacts: self.contactService.contacts.value, subscribe: false) + self.contactService.removeAllContacts(for: accountId) + return true + } + func revokeDevice(deviceId: String, accountPassword password: String) { guard let accountId = self.accountService.currentAccount?.id else { self.showActionState.value = .hideLoading diff --git a/Ring/Ring/Features/Me/MeCoordinator.swift b/Ring/Ring/Features/Me/MeCoordinator.swift index 806f3e909be0f90f22c711d5cd0477359ca986f1..f0cdcd357c1210c840c90b4f56a8c0ca3c75fb83 100644 --- a/Ring/Ring/Features/Me/MeCoordinator.swift +++ b/Ring/Ring/Features/Me/MeCoordinator.swift @@ -31,6 +31,7 @@ public enum MeState: State { case blockedContacts case needToOnboard case accountRemoved + case accountModeChanged case needAccountMigration(accountId: String) } @@ -71,6 +72,8 @@ class MeCoordinator: Coordinator, StateableResponsive { self.needToOnboard() case .accountRemoved: self.accountRemoved() + case .accountModeChanged: + self.accountModeChanged() case .needAccountMigration(let accountId): self.migrateAccount(accountId: accountId) } @@ -83,6 +86,12 @@ class MeCoordinator: Coordinator, StateableResponsive { } } + func accountModeChanged() { + if let parent = self.parentCoordinator as? AppCoordinator { + parent.stateSubject.onNext(AppState.accountModeSwitched) + } + } + func migrateAccount(accountId: String) { if let parent = self.parentCoordinator as? AppCoordinator { parent.stateSubject.onNext(AppState.needAccountMigration(accountId: accountId)) diff --git a/Ring/Ring/MigrateAccount/MigrateAccountViewModel.swift b/Ring/Ring/MigrateAccount/MigrateAccountViewModel.swift index e9f1b9447eecf7a2d9ea0c6074b9af23c5674050..9e1eb979d6ca482214c9772b29ae2f5f645c0273 100644 --- a/Ring/Ring/MigrateAccount/MigrateAccountViewModel.swift +++ b/Ring/Ring/MigrateAccount/MigrateAccountViewModel.swift @@ -121,7 +121,7 @@ class MigrateAccountViewModel: Stateable, ViewModel { func accountHasPassword() -> Bool { guard let account = self.accountService .getAccount(fromAccountId: registeredNamesKey) else {return true} - return AccountModelHelper(withAccount: account).havePassword + return AccountModelHelper(withAccount: account).hasPassword } // MARK: - Actions diff --git a/Ring/Ring/Protocols/ConversationNavigation.swift b/Ring/Ring/Protocols/ConversationNavigation.swift index 36bc4cbd1bc63f851f660266cd92449e5570caff..055554a3842b18403cf75a4f7da6b67193821681 100644 --- a/Ring/Ring/Protocols/ConversationNavigation.swift +++ b/Ring/Ring/Protocols/ConversationNavigation.swift @@ -35,6 +35,7 @@ enum ConversationState: State { case showContactPicker(callID: String) case fromCallToConversation(conversation: ConversationViewModel) case needAccountMigration(accountId: String) + case accountModeChanged } protocol ConversationNavigation: class { @@ -68,6 +69,8 @@ extension ConversationNavigation where Self: Coordinator, Self: StateableRespons self.presentCallController(call: call) case .needAccountMigration(let accountId): self.migrateAccount(accountId: accountId) + case .accountModeChanged: + self.accountModeChanged() default: break } @@ -79,6 +82,11 @@ extension ConversationNavigation where Self: Coordinator, Self: StateableRespons parent.stateSubject.onNext(AppState.needAccountMigration(accountId: accountId)) } } + func accountModeChanged() { + if let parent = self.parentCoordinator as? AppCoordinator { + parent.stateSubject.onNext(AppState.accountModeSwitched) + } + } func openRecordFile(conversation: ConversationModel, audioOnly: Bool) { let recordFileViewController = SendFileViewController.instantiate(with: self.injectionBag) diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings index 8f3591f83820977cfa8fef85ab36d60980ef8811..62ac328782e74ca858f210fe77a2ab999d4818d3 100644 --- a/Ring/Ring/Resources/en.lproj/Localizable.strings +++ b/Ring/Ring/Resources/en.lproj/Localizable.strings @@ -144,9 +144,12 @@ "actions.cancelAction" = "Cancel"; "actions.clearAction" = "Clear"; "actions.backAction" = "Back"; +"actions.doneAction" = "Done"; "alerts.incomingCallAllertTitle" = "Incoming call from "; "alerts.incomingCallButtonAccept" = "Accept"; "alerts.incomingCallButtonIgnore" = "Ignore"; +"actions.startAudioCall" = " Audio Call"; +"actions.startVideoCall" = " Video Call"; //Calls "calls.callItemTitle" = "Call"; @@ -203,6 +206,18 @@ "accountPage.usernameRegistering" = "Registering"; "accountPage.usernameRegisterAction" = "Register"; "accountPage.usernameRegistrationFailed" = "Registration failed. Please check password."; +"accountPage.createPassword" = "Create password"; +"accountPage.changePassword" = "Change password"; +"accountPage.oldPasswordPlaceholder" = "Enter old password"; +"accountPage.newPasswordPlaceholder" = "Enter new password"; +"accountPage.newPasswordConfirmPlaceholder" = "Confirm new password"; +"accountPage.changePasswordError" = "Password incorrect"; +"accountPage.enableBoothMode" = "Enable Booth Mode"; +"accountPage.disableBoothMode" = "Disable Booth Mode"; +"accountPage.disableBoothModeExplanation" = "Pleace provide your account password"; +"accountPage.boothModeExplanation" = "In booth mode conversation history not saved and jami functionality limited by making outgoing calls. When you enable booth mode all your conversations will be removed."; +"accountPage.noBoothMode" = "To enable Booth mode you need to create account password first."; +"accountPage.boothModeAlertMessage" = "After enabling booth mode all your conversations will be removed."; //Account "account.sipUsername" = "User Name"; diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift index 0f29ec74523962264854820b50ea1851fbcd937a..60c9bc467628570ff2a555eccd6caedbd0fb526c 100644 --- a/Ring/Ring/Services/AccountsService.swift +++ b/Ring/Ring/Services/AccountsService.swift @@ -66,6 +66,7 @@ class AccountsService: AccountAdapterDelegate { private let log = SwiftyBeaver.self let selectedAccountID = "SELECTED_ACCOUNT_ID" + let boothModeEnabled = "BOOTH_MODE_ENABLED" /** Used to register the service to daemon events, injected by constructor. @@ -153,8 +154,8 @@ class AccountsService: AccountAdapterDelegate { if index != 0 { self.accountList.remove(at: index) self.accountList.insert(currentAccount, at: 0) - currentAccountChanged.onNext(currentAccount) } + currentAccountChanged.onNext(currentAccount) } else { self.accountList.append(newAccount) currentAccountChanged.onNext(currentAccount) @@ -265,6 +266,40 @@ class AccountsService: AccountAdapterDelegate { } } + func boothMode() -> Bool { + return UserDefaults.standard.bool(forKey: boothModeEnabled) + } + + func setBoothMode(forAccount accountId: String, enable: Bool, password: String) -> Bool { + let enabled = UserDefaults.standard.bool(forKey: boothModeEnabled) + if enabled == enable { + return true + } + if !accountAdapter.passwordIsValid(accountId, password: password) { + return false + } + UserDefaults.standard.set(enable, forKey: boothModeEnabled) + let details = self.getAccountDetails(fromAccountId: accountId) + details + .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.dhtPublicIn), + withValue: (!enable).toString()) + setAccountDetails(forAccountId: accountId, withDetails: details) + return true + } + + func changePassword(forAccount accountId: String, password: String, newPassword: String) -> Bool { + let result = accountAdapter.changeAccountPassword(accountId, oldPassword: password, newPassword: newPassword) + if !result { + return false + } + let details = self.getAccountDetails(fromAccountId: accountId) + details + .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.archiveHasPassword), + withValue: (!newPassword.isEmpty).toString()) + setAccountDetails(forAccountId: accountId, withDetails: details) + return true + } + func getAccountProfile(accountId: String) -> AccountProfile? { return self.dbManager.accountProfile(for: accountId) } @@ -489,6 +524,8 @@ class AccountsService: AccountAdapterDelegate { let filename = "" if details .get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.ringtonePath)) == filename && + details + .get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.ringtoneEnabled)) == "false" && details .get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.dhtPeerDiscovery)) == "false" && details @@ -501,8 +538,11 @@ class AccountsService: AccountAdapterDelegate { .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.ringtonePath), withValue: filename) details - .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.dhtPeerDiscovery), + .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.ringtoneEnabled), withValue: "false") + details + .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.dhtPeerDiscovery), + withValue: "false") details .set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.accountPeerDiscovery), withValue: "false") diff --git a/Ring/Ring/Services/ContactsService.swift b/Ring/Ring/Services/ContactsService.swift index afe3d64308188aa65a79c1de89dfba8791733ce2..7a9abbc210f0389086f1cf5fec4dc8328929c9e2 100644 --- a/Ring/Ring/Services/ContactsService.swift +++ b/Ring/Ring/Services/ContactsService.swift @@ -124,14 +124,14 @@ class ContactsService { } } - func loadContactRequests(withAccount account: AccountModel) { + func loadContactRequests(withAccount accountId: String) { self.contactRequests.value.removeAll() //Load trust requests from daemon - let trustRequestsDictionaries = self.contactsAdapter.trustRequests(withAccountId: account.id) + let trustRequestsDictionaries = self.contactsAdapter.trustRequests(withAccountId: accountId) //Create contact requests from daemon trust requests if let contactRequests = trustRequestsDictionaries?.map({ dictionary in - return ContactRequestModel(withDictionary: dictionary, accountId: account.id) + return ContactRequestModel(withDictionary: dictionary, accountId: accountId) }) { for contactRequest in contactRequests { if self.contactRequest(withRingId: contactRequest.ringId) == nil { @@ -173,18 +173,18 @@ class ContactsService { } } - func discard(contactRequest: ContactRequestModel, withAccountId accountId: String) -> Observable<Void> { + func discard(from jamiId: String, withAccountId accountId: String) -> Observable<Void> { return Observable.create { [unowned self] observable in - let success = self.contactsAdapter.discardTrustRequest(fromContact: contactRequest.ringId, + let success = self.contactsAdapter.discardTrustRequest(fromContact: jamiId, withAccountId: accountId) //Update the Contact request list - self.removeContactRequest(withRingId: contactRequest.ringId) + self.removeContactRequest(withRingId: jamiId) if success { var event = ServiceEvent(withEventType: .contactRequestDiscarded) event.addEventInput(.accountId, value: accountId) - event.addEventInput(.uri, value: contactRequest.ringId) + event.addEventInput(.uri, value: jamiId) self.responseStream.onNext(event) observable.on(.completed) } else { @@ -380,4 +380,20 @@ extension ContactsService: ContactsAdapterDelegate { return nil } } + + func removeAllContacts(for accountId: String) { + DispatchQueue.global(qos: .background).async { + for contact in self.contacts.value { + self.contactsAdapter.removeContact(withURI: contact.hash, accountId: accountId, ban: false) + } + self.contacts.value.removeAll() + self.contactRequests.value.forEach { (request) in + self.contactsAdapter.discardTrustRequest(fromContact: request.ringId, withAccountId: accountId) + } + self.contactRequests.value.removeAll() + self.dbManager + .clearAllHistoryFor(accountId: accountId) + .subscribe().disposed(by: self.disposeBag) + } + } } diff --git a/Ring/Ring/Services/ConversationsManager.swift b/Ring/Ring/Services/ConversationsManager.swift index d5b9c3e51e29be9482600ae178fe67b83214d545..11041a06e3f1c208d38ef3678f2e7e80d375c280 100644 --- a/Ring/Ring/Services/ConversationsManager.swift +++ b/Ring/Ring/Services/ConversationsManager.swift @@ -54,6 +54,9 @@ class ConversationsManager: MessagesAdapterDelegate { return event.eventType == ServiceEventType.newIncomingMessage }) .subscribe(onNext: { [unowned self] event in + if self.accountsService.boothMode() { + return + } guard let accountId: String = event.getEventInput(ServiceEventInput.accountId), let messageContent: String = event.getEventInput(ServiceEventInput.content), let peerUri: String = event.getEventInput(ServiceEventInput.peerUri) @@ -70,6 +73,9 @@ class ConversationsManager: MessagesAdapterDelegate { return event.eventType == ServiceEventType.newOutgoingMessage }) .subscribe(onNext: { [unowned self] event in + if self.accountsService.boothMode() { + return + } guard let accountId: String = event.getEventInput(ServiceEventInput.accountId), let messageContent: String = event.getEventInput(ServiceEventInput.content), let peerUri: String = event.getEventInput(ServiceEventInput.peerUri), @@ -97,6 +103,9 @@ class ConversationsManager: MessagesAdapterDelegate { event.eventType == ServiceEventType.dataTransferChanged }) .subscribe(onNext: { [unowned self] event in + if self.accountsService.boothMode() { + return + } guard let transferId: UInt64 = event.getEventInput(ServiceEventInput.transferId), let transferInfo = self.dataTransferService.getTransferInfo(withId: transferId), let currentAccount = self.accountsService.currentAccount else { @@ -161,6 +170,9 @@ class ConversationsManager: MessagesAdapterDelegate { func didReceiveMessage(_ message: [String: String], from senderAccount: String, messageId: String, to receiverAccountId: String) { + if self.accountsService.boothMode() { + return + } guard let content = message[textPlainMIMEType] else { return } diff --git a/Ring/Ring/Services/GeneratedInteractionsManager.swift b/Ring/Ring/Services/GeneratedInteractionsManager.swift index 964d1956eb1d92ee174294ea050a1fcef68b1603..61b6e965dced786cb08120eb4ec48f3426e19c8e 100644 --- a/Ring/Ring/Services/GeneratedInteractionsManager.swift +++ b/Ring/Ring/Services/GeneratedInteractionsManager.swift @@ -38,10 +38,14 @@ class GeneratedInteractionsManager { self.subscribeToCallEvents() } + // swiftlint:disable cyclomatic_complexity private func subscribeToContactEvents() { self.contactService .sharedResponseStream .subscribe(onNext: { [unowned self] contactRequestEvent in + if self.accountService.boothMode() { + return + } guard let accountID: String = contactRequestEvent.getEventInput(.accountId) else { return } @@ -108,6 +112,9 @@ class GeneratedInteractionsManager { self.callService .sharedResponseStream .subscribe(onNext: { [unowned self] callEvent in + if self.accountService.boothMode() { + return + } guard let accountID: String = callEvent.getEventInput(.accountId) else { return } diff --git a/Ring/Ring/Services/PresenceService.swift b/Ring/Ring/Services/PresenceService.swift index 1eee5762a4d9dc19e38bbcdaa9aec5c76969999a..25fdc5a4362861fb0a21cf01baddbb1f233aae78 100644 --- a/Ring/Ring/Services/PresenceService.swift +++ b/Ring/Ring/Services/PresenceService.swift @@ -27,11 +27,16 @@ class PresenceService { fileprivate let log = SwiftyBeaver.self var contactPresence: [String: Variable<Bool>] + fileprivate let responseStream = PublishSubject<ServiceEvent>() + var sharedResponseStream: Observable<ServiceEvent> + fileprivate let disposeBag = DisposeBag() init(withPresenceAdapter presenceAdapter: PresenceAdapter) { self.contactPresence = [String: Variable<Bool>]() self.presenceAdapter = presenceAdapter + self.responseStream.disposed(by: disposeBag) + self.sharedResponseStream = responseStream.share() PresenceAdapter.delegate = self } @@ -49,12 +54,20 @@ class PresenceService { withUri uri: String, withFlag flag: Bool) { presenceAdapter.subscribeBuddy(withURI: uri, withAccountId: accountId, withFlag: flag) + if !flag { + contactPresence[uri] = nil + return + } if let presenceForContact = contactPresence[uri] { presenceForContact.value = false return } let observableValue = Variable<Bool>(false) contactPresence[uri] = observableValue + var event = ServiceEvent(withEventType: .presenseSubscribed) + event.addEventInput(.accountId, value: accountId) + event.addEventInput(.uri, value: uri) + self.responseStream.onNext(event) } } diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift index 8be118dd78209e5bb62ea905e6ec9ac4c4b21fbe..eb7132cf1ec08e5ae4b56a825480acc67fcee06f 100644 --- a/Ring/Ring/Services/ServiceEvent.swift +++ b/Ring/Ring/Services/ServiceEvent.swift @@ -51,6 +51,7 @@ enum ServiceEventType { case messageTypingIndicator case migrationEnded case lastDisplayedMessageUpdated + case presenseSubscribed } /**