From c7ae47f85fbe5466bb57a873c8abc4ce9109faea Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Wed, 3 Apr 2024 12:16:01 +0100 Subject: [PATCH 1/7] New iOS Client --- client-ios/.gitignore | 134 ----- client-ios/Podfile | 10 - client-ios/Podfile.lock | 20 - client-ios/TheApp.xcodeproj/project.pbxproj | 549 ------------------ client-ios/TheApp/AppDelegate.swift | 39 -- .../AppIcon.appiconset/Contents.json | 1 - .../logo.imageset/Contents.json | 21 - .../Assets.xcassets/logo.imageset/logo.png | Bin 68395 -> 0 bytes .../TheApp/Base.lproj/LaunchScreen.storyboard | 43 -- client-ios/TheApp/Helpers/Constants.swift | 19 - .../TheApp/Helpers/DelegateExtensions.swift | 31 - client-ios/TheApp/Helpers/Extensions.swift | 131 ----- client-ios/TheApp/Helpers/RemoteLoader.swift | 74 --- .../TheApp/Helpers/VDateFormatter.swift | 19 - .../TheApp/ImageCollectionViewCell.swift | 90 --- client-ios/TheApp/Info.plist | 64 -- .../TheApp/InfoCollectionViewCell.swift | 64 -- client-ios/TheApp/Models/ClientManager.swift | 188 ------ client-ios/TheApp/Models/Models.swift | 158 ----- client-ios/TheApp/Models/VTabBarItem.swift | 48 -- client-ios/TheApp/SceneDelegate.swift | 70 --- client-ios/TheApp/SpinnerView.swift | 63 -- .../TheApp/TextCollectionViewCell.swift | 68 --- client-ios/TheApp/VButton.swift | 32 - client-ios/TheApp/VProfilePictureView.swift | 51 -- client-ios/TheApp/VTextField.swift | 20 - .../Auth/LoginViewController.swift | 135 ----- .../Auth/SignupViewController.swift | 84 --- .../ViewControllers/CallViewController.swift | 291 ---------- .../Chat/ChatListViewController.swift | 117 ---- .../Chat/ChatViewController.swift | 303 ---------- .../ViewControllers/ChatViewController.swift | 234 -------- .../ContactsViewController.swift.swift | 78 --- .../ConversationListViewController.swift | 78 --- .../CreateConversationViewController.swift | 118 ---- .../ViewControllers/HomeViewController.swift | 178 ------ .../ViewControllers/ListViewController.swift | 141 ----- .../ViewControllers/LoginViewController.swift | 114 ---- .../SettingsViewController.swift.swift | 155 ----- .../UserDetailViewController.swift | 81 --- client-ios/Vapp.xcodeproj/project.pbxproj | 462 +++++++++++++++ .../xcshareddata/xcschemes/Vapp.xcscheme | 77 +++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/180.png | Bin .../Assets.xcassets/AppIcon.appiconset/20.png | Bin .../Assets.xcassets/AppIcon.appiconset/29.png | Bin .../Assets.xcassets/AppIcon.appiconset/40.png | Bin .../Assets.xcassets/AppIcon.appiconset/50.png | Bin .../Assets.xcassets/AppIcon.appiconset/57.png | Bin .../Assets.xcassets/AppIcon.appiconset/58.png | Bin .../Assets.xcassets/AppIcon.appiconset/60.png | Bin .../Assets.xcassets/AppIcon.appiconset/72.png | Bin .../Assets.xcassets/AppIcon.appiconset/76.png | Bin .../Assets.xcassets/AppIcon.appiconset/80.png | Bin .../Assets.xcassets/AppIcon.appiconset/87.png | Bin .../AppIcon.appiconset/Contents.json | 158 +++++ .../Assets.xcassets/Contents.json | 0 client-ios/Vapp/Auth/LogInView.swift | 78 +++ client-ios/Vapp/Auth/SignUpView.swift | 63 ++ client-ios/Vapp/CallView.swift | 151 +++++ client-ios/Vapp/ChatInviteView.swift | 81 +++ client-ios/Vapp/ChatView.swift | 230 ++++++++ client-ios/Vapp/CreateConversationView.swift | 77 +++ client-ios/Vapp/Helpers/ClientManager.swift | 191 ++++++ client-ios/Vapp/Helpers/Constants.swift | 10 + client-ios/Vapp/Helpers/Models.swift | 84 +++ client-ios/Vapp/Helpers/RemoteLoader.swift | 53 ++ client-ios/Vapp/Home/HomeView.swift | 122 ++++ client-ios/Vapp/Home/HomeViewModel.swift | 140 +++++ client-ios/Vapp/IncomingCallView.swift | 36 ++ client-ios/Vapp/IncomingChatView.swift | 36 ++ .../Preview Assets.xcassets/Contents.json | 6 + client-ios/Vapp/SettingsView.swift | 30 + client-ios/Vapp/UsersView.swift | 69 +++ client-ios/Vapp/Vapp.swift | 17 + 82 files changed, 2182 insertions(+), 4114 deletions(-) delete mode 100644 client-ios/.gitignore delete mode 100644 client-ios/Podfile delete mode 100644 client-ios/Podfile.lock delete mode 100644 client-ios/TheApp.xcodeproj/project.pbxproj delete mode 100644 client-ios/TheApp/AppDelegate.swift delete mode 100644 client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 client-ios/TheApp/Assets.xcassets/logo.imageset/Contents.json delete mode 100644 client-ios/TheApp/Assets.xcassets/logo.imageset/logo.png delete mode 100644 client-ios/TheApp/Base.lproj/LaunchScreen.storyboard delete mode 100644 client-ios/TheApp/Helpers/Constants.swift delete mode 100644 client-ios/TheApp/Helpers/DelegateExtensions.swift delete mode 100644 client-ios/TheApp/Helpers/Extensions.swift delete mode 100644 client-ios/TheApp/Helpers/RemoteLoader.swift delete mode 100644 client-ios/TheApp/Helpers/VDateFormatter.swift delete mode 100644 client-ios/TheApp/ImageCollectionViewCell.swift delete mode 100644 client-ios/TheApp/Info.plist delete mode 100644 client-ios/TheApp/InfoCollectionViewCell.swift delete mode 100644 client-ios/TheApp/Models/ClientManager.swift delete mode 100644 client-ios/TheApp/Models/Models.swift delete mode 100644 client-ios/TheApp/Models/VTabBarItem.swift delete mode 100644 client-ios/TheApp/SceneDelegate.swift delete mode 100644 client-ios/TheApp/SpinnerView.swift delete mode 100644 client-ios/TheApp/TextCollectionViewCell.swift delete mode 100644 client-ios/TheApp/VButton.swift delete mode 100644 client-ios/TheApp/VProfilePictureView.swift delete mode 100644 client-ios/TheApp/VTextField.swift delete mode 100644 client-ios/TheApp/ViewControllers/Auth/LoginViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/Auth/SignupViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/CallViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/Chat/ChatListViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/Chat/ChatViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/ChatViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/ContactsViewController.swift.swift delete mode 100644 client-ios/TheApp/ViewControllers/ConversationListViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/CreateConversationViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/HomeViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/ListViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/LoginViewController.swift delete mode 100644 client-ios/TheApp/ViewControllers/SettingsViewController.swift.swift delete mode 100644 client-ios/TheApp/ViewControllers/UserDetailViewController.swift create mode 100644 client-ios/Vapp.xcodeproj/project.pbxproj create mode 100644 client-ios/Vapp.xcodeproj/xcshareddata/xcschemes/Vapp.xcscheme create mode 100644 client-ios/Vapp/Assets.xcassets/AccentColor.colorset/Contents.json rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/100.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/114.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/120.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/144.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/152.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/167.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/180.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/20.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/29.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/40.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/50.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/57.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/58.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/60.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/72.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/76.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/80.png (100%) rename client-ios/{TheApp => Vapp}/Assets.xcassets/AppIcon.appiconset/87.png (100%) create mode 100644 client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/Contents.json rename client-ios/{TheApp => Vapp}/Assets.xcassets/Contents.json (100%) create mode 100644 client-ios/Vapp/Auth/LogInView.swift create mode 100644 client-ios/Vapp/Auth/SignUpView.swift create mode 100644 client-ios/Vapp/CallView.swift create mode 100644 client-ios/Vapp/ChatInviteView.swift create mode 100644 client-ios/Vapp/ChatView.swift create mode 100644 client-ios/Vapp/CreateConversationView.swift create mode 100644 client-ios/Vapp/Helpers/ClientManager.swift create mode 100644 client-ios/Vapp/Helpers/Constants.swift create mode 100644 client-ios/Vapp/Helpers/Models.swift create mode 100644 client-ios/Vapp/Helpers/RemoteLoader.swift create mode 100644 client-ios/Vapp/Home/HomeView.swift create mode 100644 client-ios/Vapp/Home/HomeViewModel.swift create mode 100644 client-ios/Vapp/IncomingCallView.swift create mode 100644 client-ios/Vapp/IncomingChatView.swift create mode 100644 client-ios/Vapp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 client-ios/Vapp/SettingsView.swift create mode 100644 client-ios/Vapp/UsersView.swift create mode 100644 client-ios/Vapp/Vapp.swift diff --git a/client-ios/.gitignore b/client-ios/.gitignore deleted file mode 100644 index f083bfc..0000000 --- a/client-ios/.gitignore +++ /dev/null @@ -1,134 +0,0 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,swift -# Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,swift - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Swift ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -Pods/ -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -### Xcode ### -# Xcode -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - - - - -## Gcc Patch -/*.gcno - -### Xcode Patch ### -*.xcodeproj/* -!*.xcodeproj/project.pbxproj -!*.xcodeproj/xcshareddata/ -!*.xcworkspace/contents.xcworkspacedata -**/xcshareddata/WorkspaceSettings.xcsettings - -# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,swift \ No newline at end of file diff --git a/client-ios/Podfile b/client-ios/Podfile deleted file mode 100644 index e176d28..0000000 --- a/client-ios/Podfile +++ /dev/null @@ -1,10 +0,0 @@ -# Uncomment the next line to define a global platform for your project - platform :ios, '14.0' - -target 'TheApp' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - - # Pods for TheApp - pod 'NexmoClient' -end diff --git a/client-ios/Podfile.lock b/client-ios/Podfile.lock deleted file mode 100644 index 7a1a80d..0000000 --- a/client-ios/Podfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -PODS: - - NexmoClient (4.3.1): - - VonageWebRTC (= 84.0.22) - - VonageWebRTC (84.0.22) - -DEPENDENCIES: - - NexmoClient - -SPEC REPOS: - trunk: - - NexmoClient - - VonageWebRTC - -SPEC CHECKSUMS: - NexmoClient: f3e93cb349999b2283cd4c82536ffd0c38ee4c1a - VonageWebRTC: 035f3a68ed821344b2fba6c7baa086f350e0a277 - -PODFILE CHECKSUM: 8636030fe5c784bba6eba1c02645be4d7b7ef702 - -COCOAPODS: 1.11.3 diff --git a/client-ios/TheApp.xcodeproj/project.pbxproj b/client-ios/TheApp.xcodeproj/project.pbxproj deleted file mode 100644 index beabd51..0000000 --- a/client-ios/TheApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,549 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXBuildFile section */ - 121D9D9225FA764200C36658 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121D9D9125FA764200C36658 /* Models.swift */; }; - 121D9D9525FA772200C36658 /* RemoteLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121D9D9425FA772200C36658 /* RemoteLoader.swift */; }; - 122D6F6B26A83C0E00028418 /* ContactsViewController.swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122D6F6926A83C0E00028418 /* ContactsViewController.swift.swift */; }; - 122D6F6C26A83C0E00028418 /* SettingsViewController.swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122D6F6A26A83C0E00028418 /* SettingsViewController.swift.swift */; }; - 122D6F6E26A999AA00028418 /* UserDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122D6F6D26A999AA00028418 /* UserDetailViewController.swift */; }; - 122D6F7026B0426100028418 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122D6F6F26B0426100028418 /* Constants.swift */; }; - 1248413925FB8A93009DBEB9 /* ClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1248413825FB8A93009DBEB9 /* ClientManager.swift */; }; - 12665CD026BD48E1000DD42E /* ChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12665CCF26BD48E1000DD42E /* ChatListViewController.swift */; }; - 12665CD426BD4B95000DD42E /* TextCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12665CD326BD4B95000DD42E /* TextCollectionViewCell.swift */; }; - 12665CD826BD737C000DD42E /* InfoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12665CD726BD737C000DD42E /* InfoCollectionViewCell.swift */; }; - 12665CDA26BD9C5A000DD42E /* VDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12665CD926BD9C5A000DD42E /* VDateFormatter.swift */; }; - 12665CDC26C13E6B000DD42E /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12665CDB26C13E6B000DD42E /* ImageCollectionViewCell.swift */; }; - 127E728C26A5D0CF00737906 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127E728B26A5D0CF00737906 /* HomeViewController.swift */; }; - 127E729026A5D60100737906 /* VTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127E728F26A5D60100737906 /* VTabBarItem.swift */; }; - 1287D94526FCDBDC004D87F9 /* VButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1287D94426FCDBDC004D87F9 /* VButton.swift */; }; - 1297BDDD2601227F00025D12 /* CreateConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1297BDDC2601227F00025D12 /* CreateConversationViewController.swift */; }; - 1297BDE0260123AB00025D12 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1297BDDF260123AB00025D12 /* ListViewController.swift */; }; - 1297BDE426026A4E00025D12 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1297BDE326026A4E00025D12 /* ChatViewController.swift */; }; - 12B77902250FA8BE0081D8AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B77901250FA8BE0081D8AF /* AppDelegate.swift */; }; - 12B77904250FA8BE0081D8AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B77903250FA8BE0081D8AF /* SceneDelegate.swift */; }; - 12B77906250FA8BE0081D8AF /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B77905250FA8BE0081D8AF /* LoginViewController.swift */; }; - 12B7790B250FA8C10081D8AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 12B7790A250FA8C10081D8AF /* Assets.xcassets */; }; - 12B7790E250FA8C10081D8AF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 12B7790C250FA8C10081D8AF /* LaunchScreen.storyboard */; }; - 12D2385625FFE409004D1FC6 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D2385525FFE409004D1FC6 /* Extensions.swift */; }; - 12D2385925FFE42A004D1FC6 /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D2385825FFE42A004D1FC6 /* SpinnerView.swift */; }; - 12D238602600E6BD004D1FC6 /* VTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D2385F2600E6BD004D1FC6 /* VTextField.swift */; }; - 12D2386E2600F371004D1FC6 /* ConversationListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D2386D2600F371004D1FC6 /* ConversationListViewController.swift */; }; - 12EA0F3426B1A9BD00E41537 /* DelegateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EA0F3326B1A9BD00E41537 /* DelegateExtensions.swift */; }; - 12EA0F3626B1AD4700E41537 /* CallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EA0F3526B1AD4700E41537 /* CallViewController.swift */; }; - 12EA0F3A26B3056400E41537 /* VProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EA0F3926B3056400E41537 /* VProfilePictureView.swift */; }; - 12FABB3425FFBF9F00B61567 /* SignupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12FABB3325FFBF9F00B61567 /* SignupViewController.swift */; }; - 801DA66827ABF3D56EA9B62A /* Pods_TheApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA80DEE4ED3F73D8F1D8F54E /* Pods_TheApp.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 121D9D9125FA764200C36658 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; - 121D9D9425FA772200C36658 /* RemoteLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteLoader.swift; sourceTree = ""; }; - 122D6F6926A83C0E00028418 /* ContactsViewController.swift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsViewController.swift.swift; sourceTree = ""; }; - 122D6F6A26A83C0E00028418 /* SettingsViewController.swift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift.swift; sourceTree = ""; }; - 122D6F6D26A999AA00028418 /* UserDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailViewController.swift; sourceTree = ""; }; - 122D6F6F26B0426100028418 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 1248413825FB8A93009DBEB9 /* ClientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientManager.swift; sourceTree = ""; }; - 12665CCF26BD48E1000DD42E /* ChatListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewController.swift; sourceTree = ""; }; - 12665CD326BD4B95000DD42E /* TextCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollectionViewCell.swift; sourceTree = ""; }; - 12665CD726BD737C000DD42E /* InfoCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCollectionViewCell.swift; sourceTree = ""; }; - 12665CD926BD9C5A000DD42E /* VDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDateFormatter.swift; sourceTree = ""; }; - 12665CDB26C13E6B000DD42E /* ImageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCollectionViewCell.swift; sourceTree = ""; }; - 127E728B26A5D0CF00737906 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; - 127E728F26A5D60100737906 /* VTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VTabBarItem.swift; sourceTree = ""; }; - 1287D94426FCDBDC004D87F9 /* VButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VButton.swift; sourceTree = ""; }; - 1297BDDC2601227F00025D12 /* CreateConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateConversationViewController.swift; sourceTree = ""; }; - 1297BDDF260123AB00025D12 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; - 1297BDE326026A4E00025D12 /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; - 12B778FE250FA8BE0081D8AF /* TheApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TheApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 12B77901250FA8BE0081D8AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 12B77903250FA8BE0081D8AF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 12B77905250FA8BE0081D8AF /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; - 12B7790A250FA8C10081D8AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 12B7790D250FA8C10081D8AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 12B7790F250FA8C10081D8AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 12D2385525FFE409004D1FC6 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - 12D2385825FFE42A004D1FC6 /* SpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = ""; }; - 12D2385F2600E6BD004D1FC6 /* VTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VTextField.swift; sourceTree = ""; }; - 12D2386D2600F371004D1FC6 /* ConversationListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationListViewController.swift; sourceTree = ""; }; - 12EA0F3326B1A9BD00E41537 /* DelegateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateExtensions.swift; sourceTree = ""; }; - 12EA0F3526B1AD4700E41537 /* CallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewController.swift; sourceTree = ""; }; - 12EA0F3926B3056400E41537 /* VProfilePictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VProfilePictureView.swift; sourceTree = ""; }; - 12FABB3325FFBF9F00B61567 /* SignupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupViewController.swift; sourceTree = ""; }; - 61C588AC4C8E10B991F32309 /* Pods-TheApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TheApp.release.xcconfig"; path = "Target Support Files/Pods-TheApp/Pods-TheApp.release.xcconfig"; sourceTree = ""; }; - AFF66C2E8B6C7F1876623B97 /* Pods-TheApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TheApp.debug.xcconfig"; path = "Target Support Files/Pods-TheApp/Pods-TheApp.debug.xcconfig"; sourceTree = ""; }; - EA80DEE4ED3F73D8F1D8F54E /* Pods_TheApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TheApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 12B778FB250FA8BE0081D8AF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 801DA66827ABF3D56EA9B62A /* Pods_TheApp.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 12665CD526BD56CD000DD42E /* Auth */ = { - isa = PBXGroup; - children = ( - 12B77905250FA8BE0081D8AF /* LoginViewController.swift */, - 12FABB3325FFBF9F00B61567 /* SignupViewController.swift */, - ); - path = Auth; - sourceTree = ""; - }; - 12665CD626BD56E4000DD42E /* Chat */ = { - isa = PBXGroup; - children = ( - 1297BDE326026A4E00025D12 /* ChatViewController.swift */, - 12665CCF26BD48E1000DD42E /* ChatListViewController.swift */, - ); - path = Chat; - sourceTree = ""; - }; - 12665CDD26C13EA6000DD42E /* CollectionViewCells */ = { - isa = PBXGroup; - children = ( - 12665CD326BD4B95000DD42E /* TextCollectionViewCell.swift */, - 12665CDB26C13E6B000DD42E /* ImageCollectionViewCell.swift */, - 12665CD726BD737C000DD42E /* InfoCollectionViewCell.swift */, - ); - name = CollectionViewCells; - sourceTree = ""; - }; - 12B778F5250FA8BE0081D8AF = { - isa = PBXGroup; - children = ( - 12B77900250FA8BE0081D8AF /* TheApp */, - 12B778FF250FA8BE0081D8AF /* Products */, - F272A9AD152B94CE2D876623 /* Pods */, - 99F8A77F36B2101A6F7104DF /* Frameworks */, - ); - sourceTree = ""; - }; - 12B778FF250FA8BE0081D8AF /* Products */ = { - isa = PBXGroup; - children = ( - 12B778FE250FA8BE0081D8AF /* TheApp.app */, - ); - name = Products; - sourceTree = ""; - }; - 12B77900250FA8BE0081D8AF /* TheApp */ = { - isa = PBXGroup; - children = ( - 12D2385B2600E676004D1FC6 /* ViewControllers */, - 12D2385D2600E69E004D1FC6 /* Views */, - 12D238622600E6F5004D1FC6 /* Helpers */, - 12D238702600F3AF004D1FC6 /* Models */, - 12B77901250FA8BE0081D8AF /* AppDelegate.swift */, - 12B77903250FA8BE0081D8AF /* SceneDelegate.swift */, - 12B7790A250FA8C10081D8AF /* Assets.xcassets */, - 12B7790C250FA8C10081D8AF /* LaunchScreen.storyboard */, - 12B7790F250FA8C10081D8AF /* Info.plist */, - ); - path = TheApp; - sourceTree = ""; - }; - 12D2385B2600E676004D1FC6 /* ViewControllers */ = { - isa = PBXGroup; - children = ( - 12665CD526BD56CD000DD42E /* Auth */, - 12665CD626BD56E4000DD42E /* Chat */, - 12D2386D2600F371004D1FC6 /* ConversationListViewController.swift */, - 1297BDDC2601227F00025D12 /* CreateConversationViewController.swift */, - 1297BDDF260123AB00025D12 /* ListViewController.swift */, - 127E728B26A5D0CF00737906 /* HomeViewController.swift */, - 122D6F6926A83C0E00028418 /* ContactsViewController.swift.swift */, - 122D6F6A26A83C0E00028418 /* SettingsViewController.swift.swift */, - 122D6F6D26A999AA00028418 /* UserDetailViewController.swift */, - 12EA0F3526B1AD4700E41537 /* CallViewController.swift */, - ); - path = ViewControllers; - sourceTree = ""; - }; - 12D2385D2600E69E004D1FC6 /* Views */ = { - isa = PBXGroup; - children = ( - 12665CDD26C13EA6000DD42E /* CollectionViewCells */, - 12D2385825FFE42A004D1FC6 /* SpinnerView.swift */, - 12D2385F2600E6BD004D1FC6 /* VTextField.swift */, - 12EA0F3926B3056400E41537 /* VProfilePictureView.swift */, - 1287D94426FCDBDC004D87F9 /* VButton.swift */, - ); - name = Views; - sourceTree = ""; - }; - 12D238622600E6F5004D1FC6 /* Helpers */ = { - isa = PBXGroup; - children = ( - 121D9D9425FA772200C36658 /* RemoteLoader.swift */, - 12D2385525FFE409004D1FC6 /* Extensions.swift */, - 122D6F6F26B0426100028418 /* Constants.swift */, - 12EA0F3326B1A9BD00E41537 /* DelegateExtensions.swift */, - 12665CD926BD9C5A000DD42E /* VDateFormatter.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - 12D238702600F3AF004D1FC6 /* Models */ = { - isa = PBXGroup; - children = ( - 1248413825FB8A93009DBEB9 /* ClientManager.swift */, - 121D9D9125FA764200C36658 /* Models.swift */, - 127E728F26A5D60100737906 /* VTabBarItem.swift */, - ); - path = Models; - sourceTree = ""; - }; - 99F8A77F36B2101A6F7104DF /* Frameworks */ = { - isa = PBXGroup; - children = ( - EA80DEE4ED3F73D8F1D8F54E /* Pods_TheApp.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - F272A9AD152B94CE2D876623 /* Pods */ = { - isa = PBXGroup; - children = ( - AFF66C2E8B6C7F1876623B97 /* Pods-TheApp.debug.xcconfig */, - 61C588AC4C8E10B991F32309 /* Pods-TheApp.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 12B778FD250FA8BE0081D8AF /* TheApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 12B77912250FA8C10081D8AF /* Build configuration list for PBXNativeTarget "TheApp" */; - buildPhases = ( - 8279577C1AF961B66FE94CFB /* [CP] Check Pods Manifest.lock */, - 12B778FA250FA8BE0081D8AF /* Sources */, - 12B778FB250FA8BE0081D8AF /* Frameworks */, - 12B778FC250FA8BE0081D8AF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = TheApp; - productName = TheApp; - productReference = 12B778FE250FA8BE0081D8AF /* TheApp.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 12B778F6250FA8BE0081D8AF /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1240; - ORGANIZATIONNAME = Vonage; - TargetAttributes = { - 12B778FD250FA8BE0081D8AF = { - CreatedOnToolsVersion = 11.5; - }; - }; - }; - buildConfigurationList = 12B778F9250FA8BE0081D8AF /* Build configuration list for PBXProject "TheApp" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 12B778F5250FA8BE0081D8AF; - productRefGroup = 12B778FF250FA8BE0081D8AF /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 12B778FD250FA8BE0081D8AF /* TheApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 12B778FC250FA8BE0081D8AF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 12B7790E250FA8C10081D8AF /* LaunchScreen.storyboard in Resources */, - 12B7790B250FA8C10081D8AF /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 8279577C1AF961B66FE94CFB /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-TheApp-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 12B778FA250FA8BE0081D8AF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 12665CDA26BD9C5A000DD42E /* VDateFormatter.swift in Sources */, - 12EA0F3626B1AD4700E41537 /* CallViewController.swift in Sources */, - 12D238602600E6BD004D1FC6 /* VTextField.swift in Sources */, - 12665CD826BD737C000DD42E /* InfoCollectionViewCell.swift in Sources */, - 12B77906250FA8BE0081D8AF /* LoginViewController.swift in Sources */, - 12D2385625FFE409004D1FC6 /* Extensions.swift in Sources */, - 12B77902250FA8BE0081D8AF /* AppDelegate.swift in Sources */, - 12665CD426BD4B95000DD42E /* TextCollectionViewCell.swift in Sources */, - 12FABB3425FFBF9F00B61567 /* SignupViewController.swift in Sources */, - 127E728C26A5D0CF00737906 /* HomeViewController.swift in Sources */, - 12665CDC26C13E6B000DD42E /* ImageCollectionViewCell.swift in Sources */, - 127E729026A5D60100737906 /* VTabBarItem.swift in Sources */, - 1248413925FB8A93009DBEB9 /* ClientManager.swift in Sources */, - 1297BDE0260123AB00025D12 /* ListViewController.swift in Sources */, - 122D6F6B26A83C0E00028418 /* ContactsViewController.swift.swift in Sources */, - 122D6F6C26A83C0E00028418 /* SettingsViewController.swift.swift in Sources */, - 1297BDDD2601227F00025D12 /* CreateConversationViewController.swift in Sources */, - 12B77904250FA8BE0081D8AF /* SceneDelegate.swift in Sources */, - 12D2385925FFE42A004D1FC6 /* SpinnerView.swift in Sources */, - 12D2386E2600F371004D1FC6 /* ConversationListViewController.swift in Sources */, - 1287D94526FCDBDC004D87F9 /* VButton.swift in Sources */, - 122D6F7026B0426100028418 /* Constants.swift in Sources */, - 121D9D9525FA772200C36658 /* RemoteLoader.swift in Sources */, - 12665CD026BD48E1000DD42E /* ChatListViewController.swift in Sources */, - 12EA0F3426B1A9BD00E41537 /* DelegateExtensions.swift in Sources */, - 122D6F6E26A999AA00028418 /* UserDetailViewController.swift in Sources */, - 1297BDE426026A4E00025D12 /* ChatViewController.swift in Sources */, - 12EA0F3A26B3056400E41537 /* VProfilePictureView.swift in Sources */, - 121D9D9225FA764200C36658 /* Models.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 12B7790C250FA8C10081D8AF /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 12B7790D250FA8C10081D8AF /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 12B77910250FA8C10081D8AF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.5; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 12B77911250FA8C10081D8AF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.5; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 12B77913250FA8C10081D8AF /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AFF66C2E8B6C7F1876623B97 /* Pods-TheApp.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7F2B5ZSP8Q; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = TheApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.vonage.TheApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 12B77914250FA8C10081D8AF /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 61C588AC4C8E10B991F32309 /* Pods-TheApp.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7F2B5ZSP8Q; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = TheApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.vonage.TheApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 12B778F9250FA8BE0081D8AF /* Build configuration list for PBXProject "TheApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 12B77910250FA8C10081D8AF /* Debug */, - 12B77911250FA8C10081D8AF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 12B77912250FA8C10081D8AF /* Build configuration list for PBXNativeTarget "TheApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 12B77913250FA8C10081D8AF /* Debug */, - 12B77914250FA8C10081D8AF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 12B778F6250FA8BE0081D8AF /* Project object */; -} diff --git a/client-ios/TheApp/AppDelegate.swift b/client-ios/TheApp/AppDelegate.swift deleted file mode 100644 index aab9a27..0000000 --- a/client-ios/TheApp/AppDelegate.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// AppDelegate.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 14/09/2020. -// Copyright © 2020 Vonage. All rights reserved. -// - -import UIKit -import AVFoundation - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - AVAudioSession.sharedInstance().requestRecordPermission { granted in - print("Allow microphone use. Response: %d", granted) - } - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 65b74d7..0000000 --- a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1 +0,0 @@ -{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} \ No newline at end of file diff --git a/client-ios/TheApp/Assets.xcassets/logo.imageset/Contents.json b/client-ios/TheApp/Assets.xcassets/logo.imageset/Contents.json deleted file mode 100644 index 5f670ca..0000000 --- a/client-ios/TheApp/Assets.xcassets/logo.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "logo.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/client-ios/TheApp/Assets.xcassets/logo.imageset/logo.png b/client-ios/TheApp/Assets.xcassets/logo.imageset/logo.png deleted file mode 100644 index 5c68fa8f6a8ec25972617defa6750771172543f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68395 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
  • S%JF4g*Spb)wl( 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client-ios/TheApp/Helpers/Constants.swift b/client-ios/TheApp/Helpers/Constants.swift deleted file mode 100644 index dd864e1..0000000 --- a/client-ios/TheApp/Helpers/Constants.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Constants.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 27/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -struct Constants { - static let keychainServer = "developer.vonage.com" - static let backgroundColor = UIColor.black - static let secondaryBackgroundColor = UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0) - static let primaryTextColor = UIColor.white - static let secondaryTextColor = UIColor.white.withAlphaComponent(0.1) - static let destructiveTextColor = UIColor.red - static let highlightColor = UIColor.purple -} diff --git a/client-ios/TheApp/Helpers/DelegateExtensions.swift b/client-ios/TheApp/Helpers/DelegateExtensions.swift deleted file mode 100644 index 59977b4..0000000 --- a/client-ios/TheApp/Helpers/DelegateExtensions.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// DelegateExtensions.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 28/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import NexmoClient - - -/* - Providing a empty default implementation of the delegate functions - allows delegate functions to be optional. This cleans up the delegate - extensions around the code base. - */ - -extension ClientManagerCallDelegate { - func clientManager(_ clientManager: ClientManager, didMakeCall call: NXMCall?) {} - func clientManager(_ clientManager: ClientManager, makeCallDidFail errorMessage: String?) {} -} - -extension ListViewControllerDelegate { - func listViewControllerDelegateDidRefresh(_: ListViewController) {} -} - -extension HomeViewControllerDelegate { - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didCreateConversation conversation: Conversations.Conversation, conversations: [Conversations.Conversation]) {} - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadConversations conversations: [Conversations.Conversation]) {} - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadUsers users: [Users.User]) {} -} diff --git a/client-ios/TheApp/Helpers/Extensions.swift b/client-ios/TheApp/Helpers/Extensions.swift deleted file mode 100644 index 3ed1cff..0000000 --- a/client-ios/TheApp/Helpers/Extensions.swift +++ /dev/null @@ -1,131 +0,0 @@ -import UIKit -import NexmoClient - -protocol LoadingViewController { - var spinnerView: SpinnerView { get } -} - -extension LoadingViewController where Self: UIViewController { - func toggleLoading() { - DispatchQueue.main.async { - self.view.isUserInteractionEnabled.toggle() - self.spinnerView.toggle() - } - } -} - -extension UIViewController { - func showErrorAlert(message: String?) { - DispatchQueue.main.async { - let alertViewController = UIAlertController(title: "Error", message: message, preferredStyle: .alert) - alertViewController.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) - self.present(alertViewController, animated: true, completion: nil) - } - } - - func hideKeyboardWhenTappedAround() { - let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard)) - tap.cancelsTouchesInView = false - view.addGestureRecognizer(tap) - } - - @objc func dismissKeyboard() { - view.endEditing(true) - } -} - -extension UIView { - func addSubviews(_ views: UIView...) { - for view in views { - self.addSubview(view) - } - } -} - -extension UIStackView { - func addArrangedSubviews(_ views: UIView...) { - for view in views { - self.addArrangedSubview(view) - } - } - - static func spacing(value: CGFloat) -> UIView { - let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) - spacerView.translatesAutoresizingMaskIntoConstraints = false - spacerView.heightAnchor.constraint(equalToConstant: value).isActive = true - return spacerView - } -} - -extension UIBarButtonItem { - var isHidden: Bool { - get { - return !self.isEnabled - } - set { - if newValue { - self.tintColor = .clear - self.isEnabled = false - self.isAccessibilityElement = false - } else { - self.tintColor = .systemBlue - self.isEnabled = true - self.isAccessibilityElement = true - } - } - } -} - -extension Notification.Name { - static let incomingCall = Notification.Name("Call") -} - -extension NSMutableData { - func appendString(_ string: String) { - if let data = string.data(using: .utf8) { - self.append(data) - } - } -} - -extension NXMMessageEvent { - func asChatMessage() -> ChatMessage { - let displayName = embeddedInfo?.user.displayName ?? "" - switch self.messageType { - case .image: - return ChatMessage(id: uuid, - sender: displayName, - content: .image(urlString: imageUrl!), - date: Date(timeIntervalSinceReferenceDate: creationDate.timeIntervalSinceReferenceDate)) - default: - return ChatMessage(id: uuid, - sender: displayName, - content: .text(content: text ?? ""), - date: Date(timeIntervalSinceReferenceDate: creationDate.timeIntervalSinceReferenceDate)) - } - } -} - -extension NXMMemberEvent { - func asChatMessage() -> ChatMessage { - let displayName = embeddedInfo?.user.displayName ?? "" - let text: String - switch state { - case .invited: - text = "\(displayName) was invited." - case .joined: - text = "\(displayName) was joined." - case .left: - text = "\(displayName) was left." - case .unknown: - fatalError("Unknown member event state.") - @unknown default: - fatalError("Unknown member event state.") - } - - return ChatMessage(id: self.uuid, - sender: displayName, - content: .info(content: text), - date: Date(timeIntervalSinceReferenceDate: self.creationDate.timeIntervalSinceReferenceDate)) - } -} diff --git a/client-ios/TheApp/Helpers/RemoteLoader.swift b/client-ios/TheApp/Helpers/RemoteLoader.swift deleted file mode 100644 index dca7020..0000000 --- a/client-ios/TheApp/Helpers/RemoteLoader.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation - -enum RemoteLoaderError: Error { - case url - case data - case api(error: APIError) - case misc(error: Error) -} - -final class RemoteLoader { - - static let baseURL = "VAPP_BASE_URL" - - static func load(path: String, - authToken: String? = nil, - body: T?, - responseType: U.Type, - completion: @escaping ((Result) -> Void)) { - guard let url = URL(string: baseURL + path) else { - completion(.failure(.url)) - return - } - - var request = URLRequest(url: url) - if let body = body, let encodedBody = try? JSONEncoder().encode(body) { - request.httpMethod = "POST" - request.httpBody = encodedBody - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - } - - if let token = authToken { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } - - URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { - if let response = try? JSONDecoder().decode(U.self, from: data) { - completion(.success(response)) - return - } else if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { - completion(.failure(.api(error: apiError))) - return - } - } - completion(.failure(.data)) - }.resume() - } - - static func fetchData(url: String, authToken: String? = nil, completion: @escaping ((Result) -> Void)) { - guard let url = URL(string: url) else { - completion(.failure(.url)) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - - if let token = authToken { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } - - URLSession.shared.dataTask(with: request) { data, response, error in - guard error == nil else { - completion(.failure(.misc(error: error!))) - return - } - - if let data = data { - completion(.success(data)) - return - } - }.resume() - } -} diff --git a/client-ios/TheApp/Helpers/VDateFormatter.swift b/client-ios/TheApp/Helpers/VDateFormatter.swift deleted file mode 100644 index d14e18c..0000000 --- a/client-ios/TheApp/Helpers/VDateFormatter.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// DateFormatter.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 06/08/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import Foundation - -struct VDateFormatter { - static var formatter = DateFormatter() - - static func dateFor(_ timeStamp: String) -> Date? { - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" - return formatter.date(from: timeStamp) - } -} diff --git a/client-ios/TheApp/ImageCollectionViewCell.swift b/client-ios/TheApp/ImageCollectionViewCell.swift deleted file mode 100644 index 2f71687..0000000 --- a/client-ios/TheApp/ImageCollectionViewCell.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// ImageCollectionViewCell.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 06/08/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class ImageTableViewCell: UITableViewCell { - - private lazy var chatImageView: UIImageView = { - let imageView = UIImageView() - imageView.clipsToBounds = true - imageView.layer.cornerRadius = 12 - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var chatBackground: UIView = { - let view = UIView() - view.layer.cornerRadius = 12 - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - backgroundColor = Constants.backgroundColor - contentView.addSubviews(chatBackground, chatImageView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - chatImageView.image = nil - } - - public func configure(with chatMessage: ChatMessage, image: UIImage?, isSender: Bool) { - chatBackground.backgroundColor = isSender ? .systemBlue : .lightGray - setImage(with: image, isSender: isSender) - } - - private func setImage(with image: UIImage?, isSender: Bool) { - guard let image = image else { return } - chatImageView.image = image - - let leadingLabelConstraint: NSLayoutConstraint - let trailingLabelConstraint: NSLayoutConstraint - - let imageHeight: CGFloat - let imageWidth: CGFloat - - if image.size.height >= image.size.width { - chatImageView.contentMode = .scaleAspectFill - imageHeight = 320 - imageWidth = 180 - } else { - chatImageView.contentMode = .scaleAspectFit - imageHeight = 180 - imageWidth = 320 - } - - if isSender { - trailingLabelConstraint = chatImageView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -16) - leadingLabelConstraint = chatImageView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: contentView.frame.width - imageWidth) - } else { - trailingLabelConstraint = chatImageView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -(contentView.frame.width - imageWidth)) - leadingLabelConstraint = chatImageView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 16) - } - - NSLayoutConstraint.activate([ - leadingLabelConstraint, - trailingLabelConstraint, - chatImageView.heightAnchor.constraint(lessThanOrEqualToConstant: imageHeight), - chatImageView.widthAnchor.constraint(lessThanOrEqualToConstant: imageWidth), - chatImageView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 16), - chatImageView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -16), - - chatBackground.topAnchor.constraint(equalTo: chatImageView.topAnchor, constant: -8), - chatBackground.leadingAnchor.constraint(equalTo: chatImageView.leadingAnchor, constant: -8), - chatBackground.trailingAnchor.constraint(equalTo: chatImageView.trailingAnchor, constant: 8), - chatBackground.bottomAnchor.constraint(equalTo: chatImageView.bottomAnchor, constant: 8) - ]) - } -} diff --git a/client-ios/TheApp/Info.plist b/client-ios/TheApp/Info.plist deleted file mode 100644 index e83ef7a..0000000 --- a/client-ios/TheApp/Info.plist +++ /dev/null @@ -1,64 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSMicrophoneUsageDescription - To make calls. - NSPhotoLibraryUsageDescription - To add a profile picture. - - diff --git a/client-ios/TheApp/InfoCollectionViewCell.swift b/client-ios/TheApp/InfoCollectionViewCell.swift deleted file mode 100644 index 2a61cdb..0000000 --- a/client-ios/TheApp/InfoCollectionViewCell.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// InfoCollectionViewCell.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 06/08/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class InfoTableViewCell: UITableViewCell { - - private lazy var chatTextLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 1 - label.textAlignment = .center - label.font = label.font.withSize(16) - label.translatesAutoresizingMaskIntoConstraints = false - label.backgroundColor = .lightGray - return label - }() - - private lazy var chatBackground: UIView = { - let view = UIView() - view.layer.cornerRadius = 12 - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - backgroundColor = Constants.backgroundColor - contentView.addSubviews(chatBackground, chatTextLabel) - - NSLayoutConstraint.activate([ - chatTextLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 16), - chatTextLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - chatTextLabel.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -16), - - chatBackground.topAnchor.constraint(equalTo: chatTextLabel.topAnchor, constant: -8), - chatBackground.leadingAnchor.constraint(equalTo: chatTextLabel.leadingAnchor, constant: -8), - chatBackground.trailingAnchor.constraint(equalTo: chatTextLabel.trailingAnchor, constant: 8), - chatBackground.bottomAnchor.constraint(equalTo: chatTextLabel.bottomAnchor, constant: 8) - ]) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - chatTextLabel.text = nil - } - - public func configure(with chatMessage: ChatMessage) { - chatBackground.backgroundColor = .lightGray - - if case let .info(info) = chatMessage.content { - chatTextLabel.text = info - chatTextLabel.sizeToFit() - } - } -} diff --git a/client-ios/TheApp/Models/ClientManager.swift b/client-ios/TheApp/Models/ClientManager.swift deleted file mode 100644 index e0b97e5..0000000 --- a/client-ios/TheApp/Models/ClientManager.swift +++ /dev/null @@ -1,188 +0,0 @@ -import NexmoClient - -protocol ClientManagerDelegate: AnyObject { - func clientManager(_ clientManager: ClientManager, responseForAuth response: Auth.Response) - func clientManager(_ clientManager: ClientManager, authDidFail errorMessage: String?) -} - -protocol ClientManagerCallDelegate: AnyObject { - func clientManager(_ clientManager: ClientManager, didMakeCall call: NXMCall?) - func clientManager(_ clientManager: ClientManager, makeCallDidFail errorMessage: String?) -} - -protocol ClientManagerConversationDelegate: AnyObject { - func clientManager(_ clientManager: ClientManager, didGetConversation conversation: NXMConversation?) - func clientManager(_ clientManager: ClientManager, getConversationDidFail errorMessage: String?) -} - -protocol ClientManagerIncomingCallDelegate: AnyObject { - func clientManager(_ clientManager: ClientManager, didReceiveCall call: NXMCall) -} - -final class ClientManager: NSObject { - - static let shared = ClientManager() - - public var token: String { - return NXMClient.shared.authToken ?? "" - } - - private var response: Auth.Response? - public var user: Users.User? - - weak var delegate: ClientManagerDelegate? - weak var callDelegate: ClientManagerCallDelegate? - weak var incomingCallDelegate: ClientManagerIncomingCallDelegate? - weak var conversationDelegate: ClientManagerConversationDelegate? - - override init() { - super.init() - initializeClient() - } - - private func initializeClient() { - NXMClient.shared.setDelegate(self) - } - - func auth(username: String, password: String, displayName: String?, url: String, storeCredentials: Bool = true) { - let user = Auth.Body(name: username, password: password, displayName: displayName) - - RemoteLoader.load(path: url, body: user, responseType: Auth.Response.self) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let response): - self.response = response - self.user = response.user - NXMClient.shared.login(withAuthToken: response.token) - if storeCredentials { - self.storeCredentials(username: username, password: password) - } - case .failure(let error): - switch error { - case .api(error: let apiError): - if !storeCredentials { - self.deleteCredentials(username: username) - } - self.delegate?.clientManager(self, authDidFail: apiError.description) - default: - self.delegate?.clientManager(self, authDidFail: error.localizedDescription) - } - } - } - } - - func call(name: String) { - NXMClient.shared.serverCall(withCallee: name, customData: nil) { [weak self] error, call in - guard let self = self else { return } - if error != nil { - self.callDelegate?.clientManager(self, makeCallDidFail: error?.localizedDescription) - return - } - - self.callDelegate?.clientManager(self, didMakeCall: call) - } - } - - func getConversation(conversationID: String) { - NXMClient.shared.getConversationWithUuid(conversationID) { [weak self] error, conversation in - guard let self = self else { return } - if error != nil { - self.conversationDelegate?.clientManager(self, getConversationDidFail: error?.localizedDescription) - return - } - - self.conversationDelegate?.clientManager(self, didGetConversation: conversation) - } - } - - func uploadImage(imageData: Data, completionHandler: @escaping ((Error?, String?) -> Void)) { - NXMClient.shared.uploadAttachment(with: .image, name: UUID().uuidString, data: imageData) { error, responseData in - if let error = error { - completionHandler(error, nil) - return - } else { - if let imageObject = responseData?["original"] as? [String: Any], - let imageUrl = imageObject["url"] as? String { - completionHandler(nil, imageUrl) - return - } - } - completionHandler(nil, nil) - } - } - - func logout() { - deleteCredentials(username: user?.name ?? "") - NXMClient.shared.logout() - } -} - -extension ClientManager { - private func storeCredentials(username: String, password: String) { - if let passwordData = password.data(using: .utf8) { - let keychainItem = [ - kSecClass: kSecClassInternetPassword, - kSecAttrServer: Constants.keychainServer, - kSecReturnData: true, - kSecReturnAttributes: true, - kSecAttrAccount: username, - kSecValueData: passwordData - ] as CFDictionary - - let status = SecItemAdd(keychainItem, nil) - print("Keychain storing finished with status: \(status)") - } - } - - func getCredentials() -> (String, String)? { - let query = [ - kSecClass: kSecClassInternetPassword, - kSecAttrServer: Constants.keychainServer, - kSecReturnAttributes: true, - kSecReturnData: true, - kSecMatchLimit: 1 - ] as CFDictionary - - var result: AnyObject? - let status = SecItemCopyMatching(query, &result) - print("Keychain querying finished with status: \(status)") - - if let resultArray = result as? NSDictionary, - let username = resultArray[kSecAttrAccount] as? String, - let passwordData = resultArray[kSecValueData] as? Data, - let password = String(data: passwordData, encoding: .utf8) { - return (username, password) - } else { - return nil - } - } - - private func deleteCredentials(username: String) { - let query = [ - kSecClass: kSecClassInternetPassword, - kSecAttrServer: Constants.keychainServer, - kSecAttrAccount: username - ] as CFDictionary - - SecItemDelete(query) - } -} - -extension ClientManager: NXMClientDelegate { - func client(_ client: NXMClient, didChange status: NXMConnectionStatus, reason: NXMConnectionStatusReason) { - switch status { - case .connected: - delegate?.clientManager(self, responseForAuth: response!) - default: - break - } - } - - func client(_ client: NXMClient, didReceiveError error: Error) { - self.delegate?.clientManager(self, authDidFail: error.localizedDescription) - } - - func client(_ client: NXMClient, didReceive call: NXMCall) { - incomingCallDelegate?.clientManager(self, didReceiveCall: call) - } -} diff --git a/client-ios/TheApp/Models/Models.swift b/client-ios/TheApp/Models/Models.swift deleted file mode 100644 index 9bbfcc9..0000000 --- a/client-ios/TheApp/Models/Models.swift +++ /dev/null @@ -1,158 +0,0 @@ -import Foundation - -enum ChatMessageContent: Hashable, Equatable { - case info(content: String) - case text(content: String) - case image(urlString: String) -} - -protocol ListViewPresentable { - var id: String { get } - var displayName: String { get } -} - -struct ChatMessage: Hashable { - let id: Int - let sender: String - let content: ChatMessageContent - let date: Date -} - -struct Setting: Hashable, ListViewPresentable { - - enum SettingType { - case picture - case logout - } - - let id: String - let displayName: String - let type: SettingType - let iconString: String -} - -struct Image: Codable { - static let path = "/image" - - struct Body: Codable { - let imageURL: String - - enum CodingKeys: String, CodingKey { - case imageURL = "image_url" - } - } -} - -struct Users: Codable, Hashable { - static let path = "/users" - - struct List: Codable { - typealias Response = [User] - } - - struct User: Codable, Hashable, ListViewPresentable { - let id: String - let name: String - let displayName: String - let imageURL: String? - - enum CodingKeys: String, CodingKey { - case id, name - case imageURL = "image_url" - case displayName = "display_name" - } - } -} - -struct Conversations: Codable { - static let path = "/conversations" - - struct Create: Codable { - struct Body: Codable { - let users: [String] - } - - typealias Response = Conversation - } - - struct List: Codable { - typealias Response = [Conversation] - } - - struct Decorate: Codable { - typealias Response = Conversation - } - - struct Conversation: Codable, Hashable, ListViewPresentable { - let state: String - let id: String - let createdAt: String - let joinedAt: String? - let displayName: String - let users: [Users.User] - let events: [Event]? - - enum CodingKeys: String, CodingKey { - case state, id, users, events - case displayName = "name" - case createdAt = "created_at" - case joinedAt = "joined_at" - } - - struct Event: Codable, Hashable { - let id: String - let from: String - let type: String - let content: String? - let timestamp: String - } - } - -} - -struct Auth: Codable { - static let signupPath = "/signup" - static let loginPath = "/login" - - struct Body: Codable { - let name: String - let password: String - let displayName: String? - - enum CodingKeys: String, CodingKey { - case name, password - case displayName = "display_name" - } - } - - struct Response: Codable { - let user: Users.User - let token: String - let users: [Users.User] - let conversations: [Conversations.Conversation] - } -} - -struct APIError: Codable { - let type: String? - let title: String? - let detail: String? - let invalidParameters: [[String: String]]? - - enum CodingKeys: String, CodingKey { - case type, title, detail - case invalidParameters = "invalid_parameters" - } - - var description: String { - var descriptionString: String = self.detail ?? "" - - if let invalidParameters = invalidParameters { - for invalidParameter in invalidParameters { - descriptionString += "\n \(invalidParameter.description)" - } - } - - return descriptionString - } -} diff --git a/client-ios/TheApp/Models/VTabBarItem.swift b/client-ios/TheApp/Models/VTabBarItem.swift deleted file mode 100644 index 7938a6e..0000000 --- a/client-ios/TheApp/Models/VTabBarItem.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// VTabBarItem.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 19/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -enum VTabBarItem: CaseIterable { - case chats - case contacts - case settings - - var title: String { - switch self { - case .chats: - return "Chats" - case .contacts: - return "Contacts" - case .settings: - return "Settings" - } - } - - var image: UIImage? { - switch self { - case .chats: - return UIImage(systemName: "text.bubble.fill") - case .contacts: - return UIImage(systemName: "person.3.fill") - case .settings: - return UIImage(systemName: "gearshape.fill") - } - } - - var tag: Int { - switch self { - case .chats: - return 0 - case .contacts: - return 1 - case .settings: - return 2 - } - } -} diff --git a/client-ios/TheApp/SceneDelegate.swift b/client-ios/TheApp/SceneDelegate.swift deleted file mode 100644 index c34dbfb..0000000 --- a/client-ios/TheApp/SceneDelegate.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// SceneDelegate.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 14/09/2020. -// Copyright © 2020 Vonage. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let windowScene = (scene as? UIWindowScene) else { return } - - window = UIWindow(frame: windowScene.coordinateSpace.bounds) - window?.windowScene = windowScene - - let navigationController = UINavigationController(rootViewController: LoginViewController()) - navigationController.navigationBar.isTranslucent = false - navigationController.navigationBar.barTintColor = Constants.backgroundColor - navigationController.navigationBar.tintColor = Constants.highlightColor - navigationController.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.white] - - UITabBar.appearance().tintColor = Constants.highlightColor - UITabBar.appearance().barTintColor = Constants.backgroundColor - UITabBar.appearance().backgroundColor = Constants.backgroundColor - UITabBar.appearance().unselectedItemTintColor = Constants.primaryTextColor - - window?.rootViewController = navigationController - window?.makeKeyAndVisible() - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/client-ios/TheApp/SpinnerView.swift b/client-ios/TheApp/SpinnerView.swift deleted file mode 100644 index a9deee5..0000000 --- a/client-ios/TheApp/SpinnerView.swift +++ /dev/null @@ -1,63 +0,0 @@ -import UIKit - -class SpinnerView: UIView { - - private lazy var spinner: UIActivityIndicatorView = { - let spinner = UIActivityIndicatorView(style: .large) - spinner.backgroundColor = .white - spinner.color = .purple - spinner.layer.cornerRadius = 12 - spinner.translatesAutoresizingMaskIntoConstraints = false - return spinner - }() - - private lazy var detailLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.font = label.font.withSize(16) - label.numberOfLines = 0 - label.isHidden = true - label.textColor = Constants.primaryTextColor - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - init(parentView: UIView) { - super.init(frame: .zero) - self.isHidden = true - setUpConstraints(parentView: parentView) - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpConstraints(parentView: UIView) { - parentView.addSubviews(spinner, detailLabel) - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: parentView.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: parentView.centerYAnchor), - spinner.widthAnchor.constraint(equalToConstant: 48), - spinner.heightAnchor.constraint(equalToConstant: 48), - - detailLabel.centerXAnchor.constraint(equalTo: parentView.centerXAnchor), - detailLabel.topAnchor.constraint(equalTo: spinner.bottomAnchor, constant: 16) - ]) - } - - func setDetailText(text: String) { - detailLabel.isHidden = false - detailLabel.text = text - } - - func toggle() { - if self.isHidden { - self.isHidden = false - spinner.startAnimating() - } else { - self.isHidden = true - detailLabel.isHidden = true - spinner.stopAnimating() - } - } -} diff --git a/client-ios/TheApp/TextCollectionViewCell.swift b/client-ios/TheApp/TextCollectionViewCell.swift deleted file mode 100644 index 19bcd25..0000000 --- a/client-ios/TheApp/TextCollectionViewCell.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// TextCollectionViewCell.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 06/08/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class TextTableViewCell: UITableViewCell { - - private lazy var chatTextLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var chatBackground: UIView = { - let view = UIView() - view.layer.cornerRadius = 12 - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private var leadingLabelConstraint: NSLayoutConstraint! - private var trailingLabelConstraint: NSLayoutConstraint! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - backgroundColor = Constants.backgroundColor - contentView.addSubviews(chatBackground, chatTextLabel) - - NSLayoutConstraint.activate([ - chatTextLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 144), - chatTextLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 16), - chatTextLabel.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -16), - - chatBackground.topAnchor.constraint(equalTo: chatTextLabel.topAnchor, constant: -8), - chatBackground.leadingAnchor.constraint(equalTo: chatTextLabel.leadingAnchor, constant: -8), - chatBackground.trailingAnchor.constraint(equalTo: chatTextLabel.trailingAnchor, constant: 8), - chatBackground.bottomAnchor.constraint(equalTo: chatTextLabel.bottomAnchor, constant: 8) - ]) - - trailingLabelConstraint = chatTextLabel.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -16) - leadingLabelConstraint = chatTextLabel.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 16) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - chatTextLabel.text = nil - } - - public func configure(with chatMessage: ChatMessage, isSender: Bool) { - if case let .text(text) = chatMessage.content { - chatBackground.backgroundColor = isSender ? .systemBlue : .lightGray - leadingLabelConstraint.isActive = !isSender - trailingLabelConstraint.isActive = isSender - - chatTextLabel.text = text - } - } -} diff --git a/client-ios/TheApp/VButton.swift b/client-ios/TheApp/VButton.swift deleted file mode 100644 index 461798c..0000000 --- a/client-ios/TheApp/VButton.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// VButton.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 23/09/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class VButton: UIButton { - - init(title: String, isSecondary: Bool = false) { - super.init(frame: .zero) - setTitle(title, for: .normal) - titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) - layer.cornerRadius = 12 - contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) - if isSecondary { - backgroundColor = .white - setTitleColor(Constants.highlightColor, for: .normal) - layer.borderColor = Constants.highlightColor.cgColor - layer.borderWidth = 3 - } else { - backgroundColor = Constants.highlightColor - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/client-ios/TheApp/VProfilePictureView.swift b/client-ios/TheApp/VProfilePictureView.swift deleted file mode 100644 index 39db8c3..0000000 --- a/client-ios/TheApp/VProfilePictureView.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// VProfilePictureView.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 29/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class VProfilePictureView: UIImageView { - - lazy var spinnerView = SpinnerView(parentView: self) - - public var imageURL: String? { - didSet { - guard imageURL != nil else { return } - loadImage() - } - } - - init() { - super.init(frame: .zero) - self.clipsToBounds = true - self.backgroundColor = Constants.highlightColor - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = self.frame.height / 2 - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func loadImage() { - spinnerView.toggle() - RemoteLoader.fetchData(url: imageURL!, authToken: ClientManager.shared.token) { result in - DispatchQueue.main.async { - self.spinnerView.toggle() - switch result { - case .success(let data): - self.image = UIImage(data: data) - default: - break - } - } - } - } -} diff --git a/client-ios/TheApp/VTextField.swift b/client-ios/TheApp/VTextField.swift deleted file mode 100644 index 41052be..0000000 --- a/client-ios/TheApp/VTextField.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit - -class VTextField: UITextField { - init(placeholder: String, isSecure: Bool = false, isChat: Bool = false) { - super.init(frame: .zero) - self.textAlignment = .center - self.backgroundColor = Constants.secondaryBackgroundColor - self.textColor = Constants.primaryTextColor - self.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor : Constants.secondaryTextColor]) - self.isSecureTextEntry = isSecure - self.autocapitalizationType = .none - self.layer.borderWidth = isChat ? 0 : 3 - self.layer.cornerRadius = isChat ? 12 : 0 - self.layer.borderColor = UIColor.purple.cgColor - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/client-ios/TheApp/ViewControllers/Auth/LoginViewController.swift b/client-ios/TheApp/ViewControllers/Auth/LoginViewController.swift deleted file mode 100644 index bb9f5cf..0000000 --- a/client-ios/TheApp/ViewControllers/Auth/LoginViewController.swift +++ /dev/null @@ -1,135 +0,0 @@ -import UIKit - -class LoginViewController: UIViewController, LoadingViewController { - - private let usernameField = VTextField(placeholder: "Username") - private let passwordField = VTextField(placeholder: "Password", isSecure: true) - lazy var spinnerView = SpinnerView(parentView: view) - - private lazy var loginButton: UIButton = { - let button = VButton(title: "Login") - button.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var newUserButton: UIButton = { - let button = VButton(title: "New User?", isSecondary: true) - button.addTarget(self, action: #selector(newUserButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 16 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private lazy var logoView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "logo")) - imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private var loggedin = false - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - checkExistingTokenAndLogin() - hideKeyboardWhenTappedAround() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if loggedin { - resetView() - } - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - stackView.addArrangedSubviews( - usernameField, - passwordField, - UIStackView.spacing(value: 4), - loginButton, - newUserButton - ) - view.addSubviews(logoView, stackView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - logoView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), - logoView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - stackView.topAnchor.constraint(equalTo: logoView.bottomAnchor, constant: 16), - stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 96), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -96), - - logoView.heightAnchor.constraint(equalToConstant: 300), - logoView.widthAnchor.constraint(equalToConstant: 300), - usernameField.heightAnchor.constraint(equalToConstant: 48), - passwordField.heightAnchor.constraint(equalToConstant: 48) - ]) - } - - private func checkExistingTokenAndLogin() { - if let credentials = ClientManager.shared.getCredentials() { - ClientManager.shared.delegate = self - toggleLoading() - ClientManager.shared.auth(username: credentials.0, password: credentials.1, displayName: nil, url: Auth.loginPath, storeCredentials: false) - toggleViewVisibility(hidden: true) - spinnerView.setDetailText(text: "Welcome back \(credentials.0)") - } - } - - private func toggleViewVisibility(hidden: Bool) { - DispatchQueue.main.async { - self.stackView.isHidden = hidden - self.usernameField.isHidden = hidden - self.passwordField.isHidden = hidden - } - } - - private func resetView() { - loggedin = false - toggleViewVisibility(hidden: false) - } - - @objc func loginButtonTapped() { - if let username = usernameField.text, let password = passwordField.text { - ClientManager.shared.delegate = self - toggleLoading() - ClientManager.shared.auth(username: username, password: password, displayName: nil, url: Auth.loginPath) - } else { - showErrorAlert(message: "Validation error") - } - } - - @objc func newUserButtonTapped() { - let signUpViewController = SignUpViewController() - self.navigationController?.pushViewController(signUpViewController, animated: true) - } -} - -extension LoginViewController: ClientManagerDelegate { - func clientManager(_ clientManager: ClientManager, responseForAuth response: Auth.Response) { - toggleLoading() - loggedin = true - usernameField.text = "" - passwordField.text = "" - navigationController?.pushViewController(HomeViewController(data: response), animated: true) - } - - func clientManager(_ clientManager: ClientManager, authDidFail errorMessage: String?) { - toggleLoading() - resetView() - showErrorAlert(message: errorMessage) - } -} diff --git a/client-ios/TheApp/ViewControllers/Auth/SignupViewController.swift b/client-ios/TheApp/ViewControllers/Auth/SignupViewController.swift deleted file mode 100644 index 16465c8..0000000 --- a/client-ios/TheApp/ViewControllers/Auth/SignupViewController.swift +++ /dev/null @@ -1,84 +0,0 @@ -import UIKit - -class SignUpViewController: UIViewController, LoadingViewController { - - private let displayNameField = VTextField(placeholder: "Display Name") - private let usernameField = VTextField(placeholder: "Username") - private let passwordField = VTextField(placeholder: "Password", isSecure: true) - lazy var spinnerView = SpinnerView(parentView: view) - - private lazy var signUpButton: UIButton = { - let button = VButton(title: "Sign Up") - button.addTarget(self, action: #selector(signUpButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 16 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - override func viewDidLoad() { - title = "The V App" - super.viewDidLoad() - setUpView() - setUpConstraints() - hideKeyboardWhenTappedAround() - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - stackView.addArrangedSubviews( - displayNameField, - usernameField, - passwordField, - UIStackView.spacing(value: 4), - signUpButton) - view.addSubviews(stackView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 96), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -96), - - displayNameField.heightAnchor.constraint(equalToConstant: 48), - usernameField.heightAnchor.constraint(equalToConstant: 48), - passwordField.heightAnchor.constraint(equalToConstant: 48) - ]) - } - - private func toggleLoading() { - DispatchQueue.main.async { - self.view.isUserInteractionEnabled.toggle() - self.spinnerView.toggle() - } - } - - @objc func signUpButtonTapped() { - if let displayName = displayNameField.text,let username = usernameField.text, let password = passwordField.text { - ClientManager.shared.delegate = self - toggleLoading() - ClientManager.shared.auth(username: username, password: password, displayName: displayName, url: Auth.signupPath) - } else { - showErrorAlert(message: "Validation error") - } - } -} - - -extension SignUpViewController: ClientManagerDelegate { - func clientManager(_ clientManager: ClientManager, responseForAuth response: Auth.Response) { - toggleLoading() - navigationController?.pushViewController(HomeViewController(data: response), animated: true) - } - - func clientManager(_ clientManager: ClientManager, authDidFail errorMessage: String?) { - toggleLoading() - showErrorAlert(message: errorMessage) - } -} diff --git a/client-ios/TheApp/ViewControllers/CallViewController.swift b/client-ios/TheApp/ViewControllers/CallViewController.swift deleted file mode 100644 index a6facdb..0000000 --- a/client-ios/TheApp/ViewControllers/CallViewController.swift +++ /dev/null @@ -1,291 +0,0 @@ -// -// CallViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 28/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit -import NexmoClient - -class CallViewController: UIViewController { - - enum CallState { - case inactive - case ringing - case ongoing - case ended(reason: String?) - case error(reason: String?) - } - - private lazy var profilePicView: VProfilePictureView = { - let imageView = VProfilePictureView() - imageView.imageURL = user.imageURL - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var nameLabel: UILabel = { - let label = UILabel() - label.text = user.displayName - label.textAlignment = .natural - label.textColor = Constants.primaryTextColor - label.font = label.font.withSize(24) - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var callStatusLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.textColor = Constants.primaryTextColor - label.font = label.font.withSize(24) - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var endCallButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("End Call", for: .normal) - button.setTitleColor(Constants.destructiveTextColor, for: .normal) - button.isHidden = true - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(endCallButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var callButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Call", for: .normal) - button.setTitleColor(Constants.highlightColor, for: .normal) - button.isHidden = true - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(callButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var muteButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Mute", for: .normal) - button.isHidden = true - button.setImage(UIImage(systemName: "mic.slash.fill"), for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - button.tintColor = Constants.primaryTextColor - button.addTarget(self, action: #selector(muteButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var muteIconImageView: UIImageView = { - let imageView = UIImageView(image: UIImage(systemName: "mic.slash.fill")) - imageView.isHidden = true - imageView.tintColor = Constants.destructiveTextColor - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private let user: Users.User - - private var call: NXMCall? - private var isMuted = false - private var callState: CallState = .inactive { - didSet { - DispatchQueue.main.async { - self.updateUIForCallState() - } - } - } - - init(user: Users.User) { - self.user = user - super.init(nibName: nil, bundle: nil) - } - - init(call: NXMCall) { - self.call = call - guard let callUser = (call.allMembers.first { $0.user.name != ClientManager.shared.user?.name })?.user else { fatalError("Missing call member") } - self.user = Users.User( - id: callUser.uuid, - name: callUser.name, - displayName: callUser.displayName, - imageURL: callUser.imageUrl - ) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - ClientManager.shared.callDelegate = self - - if call == nil { - ClientManager.shared.call(name: user.name) - callState = .ringing - } else { - call?.answer(nil) - call?.setDelegate(self) - callState = .ongoing - } - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - callState = .ended(reason: "Call ended") - } - - private func setUpView() { - view.backgroundColor = Constants.secondaryBackgroundColor - title = "Calling \(user.displayName)" - view.addSubviews(profilePicView, nameLabel, muteIconImageView, callStatusLabel, muteButton, endCallButton, callButton) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - profilePicView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 48), - profilePicView.widthAnchor.constraint(equalToConstant: 64), - profilePicView.heightAnchor.constraint(equalToConstant: 64), - profilePicView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), - - nameLabel.centerYAnchor.constraint(equalTo: profilePicView.centerYAnchor), - nameLabel.leadingAnchor.constraint(equalTo: profilePicView.trailingAnchor, constant: 16), - nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - muteIconImageView.centerXAnchor.constraint(equalTo: profilePicView.centerXAnchor), - muteIconImageView.centerYAnchor.constraint(equalTo: profilePicView.centerYAnchor), - muteIconImageView.widthAnchor.constraint(equalToConstant: 24), - muteIconImageView.heightAnchor.constraint(equalToConstant: 24), - - callStatusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), - callStatusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - callStatusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - callStatusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - muteButton.topAnchor.constraint(equalTo: callStatusLabel.bottomAnchor, constant: 16), - muteButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - muteButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - endCallButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - endCallButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - endCallButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), - - callButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - callButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - callButton.bottomAnchor.constraint(equalTo: endCallButton.topAnchor, constant: -16) - ]) - } - - private func endCall() { - call?.hangup() - call = nil - } - - @objc private func endCallButtonTapped() { - callState = .ended(reason: "Call ended") - } - - @objc private func callButtonTapped() { - callState = .ringing - ClientManager.shared.call(name: user.name) - } - - @objc private func muteButtonTapped() { - if isMuted { - call?.unmute() - isMuted = false - muteButton.setTitle("Mute", for: .normal) - } else { - call?.mute() - isMuted = true - muteButton.setTitle("Unmute", for: .normal) - } - } - - private func updateUIForCallState() { - switch callState { - case .inactive: - callStatusLabel.text = nil - callButton.isHidden = false - endCallButton.isHidden = true - muteButton.isHidden = true - case .ringing: - callStatusLabel.text = "Ringing..." - callButton.isHidden = true - endCallButton.isHidden = false - muteButton.isHidden = true - case .ongoing: - callStatusLabel.text = "Call ongoing" - callButton.isHidden = true - endCallButton.isHidden = false - muteButton.isHidden = false - case .ended(let reason): - callStatusLabel.text = reason - callButton.isHidden = false - endCallButton.isHidden = true - muteButton.isHidden = true - endCall() - case .error(let reason): - callStatusLabel.text = reason - callButton.isHidden = false - endCallButton.isHidden = true - muteButton.isHidden = true - } - } -} - -extension CallViewController: ClientManagerCallDelegate { - func clientManager(_ clientManager: ClientManager, didMakeCall call: NXMCall?) { - DispatchQueue.main.async { - self.call = call - self.callState = .ringing - self.call?.setDelegate(self) - } - } - - func clientManager(_ clientManager: ClientManager, makeCallDidFail errorMessage: String?) { - callState = .error(reason: errorMessage) - } -} - -extension CallViewController: NXMCallDelegate { - func call(_ call: NXMCall, didUpdate member: NXMMember, with status: NXMCallMemberStatus) { - guard member.user.name == user.name else { return } - DispatchQueue.main.async { - switch status { - case .answered: - // Person called picked up - self.callState = .ongoing - case .completed: - // Person called ended the call - self.callState = .ended(reason: "\(self.user.displayName) ended the call") - case .cancelled, .rejected: - // Person called rejected the call - self.callState = .ended(reason: "\(self.user.displayName) rejected the call") - case .busy, .failed, .timeout: - // Issue with the call - self.callState = .ended(reason: "There was an error with the call, try again") - default: - break - } - } - } - - func call(_ call: NXMCall, didUpdate member: NXMMember, isMuted muted: Bool) { - guard member.user.name == user.name else { return } - DispatchQueue.main.async { - self.muteIconImageView.isHidden = !muted - } - } - - func call(_ call: NXMCall, didReceive error: Error) { - DispatchQueue.main.async { - self.callStatusLabel.text = error.localizedDescription - } - } -} diff --git a/client-ios/TheApp/ViewControllers/Chat/ChatListViewController.swift b/client-ios/TheApp/ViewControllers/Chat/ChatListViewController.swift deleted file mode 100644 index 8c0727d..0000000 --- a/client-ios/TheApp/ViewControllers/Chat/ChatListViewController.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ChatListViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 06/08/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class ChatListViewController: UIViewController { - - private lazy var tableView = makeTableView() - - private var messages: [ChatMessage] = [] - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - view.addSubview(tableView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - private func makeTableView() -> UITableView { - let tableView = UITableView(frame: .zero) - tableView.backgroundColor = Constants.backgroundColor - tableView.separatorStyle = .none - tableView.dataSource = self - tableView.allowsSelection = false - - tableView.register(TextTableViewCell.self, forCellReuseIdentifier: "TextChat") - tableView.register(InfoTableViewCell.self, forCellReuseIdentifier: "InfoChat") - tableView.register(ImageTableViewCell.self, forCellReuseIdentifier: "ImageChat") - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.transform = CGAffineTransform(scaleX: 1, y: -1) - - return tableView - } - - public func setMessages(messages: [ChatMessage]) { - self.messages = messages.reversed() - DispatchQueue.main.async { - self.tableView.reloadData() - } - } - - public func appendMessage(_ message: ChatMessage) { - messages.insert(message, at: 0) - tableView.beginUpdates() - tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) - tableView.endUpdates() - } -} - -extension ChatListViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return messages.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let message = messages[indexPath.row] - let isSender = message.sender == ClientManager.shared.user?.displayName - - switch message.content { - case .info: - let cell = tableView.dequeueReusableCell(withIdentifier: "InfoChat") as! InfoTableViewCell - cell.configure(with: message) - cell.transform = CGAffineTransform(scaleX: 1, y: -1) - return cell - case .text: - let cell = tableView.dequeueReusableCell(withIdentifier: "TextChat") as! TextTableViewCell - cell.configure(with: message, isSender: isSender) - cell.transform = CGAffineTransform(scaleX: 1, y: -1) - return cell - case .image(let urlString): - let cell = tableView.dequeueReusableCell(withIdentifier: "ImageChat") as! ImageTableViewCell - loadImageFor(cell, with: message, urlString: urlString, isSender: isSender) - cell.transform = CGAffineTransform(scaleX: 1, y: -1) - return cell - } - } - - private func loadImageFor(_ cell: ImageTableViewCell, with message: ChatMessage, urlString: String?, isSender: Bool) { - guard let urlString = urlString else { return } - RemoteLoader.fetchData(url: urlString, authToken: ClientManager.shared.token) { result in - switch result { - case .success(let data): - if let image = UIImage(data: data) { - DispatchQueue.main.async { - self.tableView.beginUpdates() - cell.configure(with: message, image: image, isSender: isSender) - self.tableView.endUpdates() - } - } - default: - break - } - } - } -} diff --git a/client-ios/TheApp/ViewControllers/Chat/ChatViewController.swift b/client-ios/TheApp/ViewControllers/Chat/ChatViewController.swift deleted file mode 100644 index 1d726eb..0000000 --- a/client-ios/TheApp/ViewControllers/Chat/ChatViewController.swift +++ /dev/null @@ -1,303 +0,0 @@ -import UIKit -import NexmoClient - -class ChatViewController: UIViewController, LoadingViewController { - /* TODO: - image send loading indicator - add message time - if group chat add sender name - */ - - private lazy var chatListViewController: ChatListViewController = { - let vc = ChatListViewController() - vc.view.translatesAutoresizingMaskIntoConstraints = false - vc.view.isHidden = true - return vc - }() - - private lazy var inputStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.distribution = .fillProportionally - stackView.spacing = 12 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private lazy var inputField: VTextField = { - let input = VTextField(placeholder: "Type a message", isChat: true) - input.delegate = self - input.returnKeyType = .send - input.translatesAutoresizingMaskIntoConstraints = false - return input - }() - - private lazy var sendButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(systemName: "arrow.up.circle.fill"), for: .normal) - button.addTarget(self, action: #selector(sendMessage), for: .touchUpInside) - button.imageView?.translatesAutoresizingMaskIntoConstraints = false - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - private lazy var imageButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(systemName: "photo.fill.on.rectangle.fill"), for: .normal) - button.addTarget(self, action: #selector(pickImage), for: .touchUpInside) - button.imageView?.translatesAutoresizingMaskIntoConstraints = false - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - private lazy var imagePicker: UIImagePickerController = { - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.allowsEditing = false - imagePicker.sourceType = .photoLibrary - return imagePicker - }() - - lazy var spinnerView = SpinnerView(parentView: view) - - private var nxmConversation: NXMConversation? - private var conversation: Conversations.Conversation - private var conversationsLoaded = (apiConv: false, nxmConv: false) { - didSet { - finishLoadingIfNeeded() - } - } - - init(conversation: Conversations.Conversation) { - self.conversation = conversation - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - - toggleLoading() - ClientManager.shared.getConversation(conversationID: conversation.id) - ClientManager.shared.conversationDelegate = self - decorateConversation() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWasShown), - name: UIResponder.keyboardDidShowNotification, object: nil) - } - - private func setUpView() { - title = conversation.displayName - view.backgroundColor = Constants.backgroundColor - view.addSubviews(chatListViewController.view, inputStackView) - inputStackView.addArrangedSubviews(imageButton, inputField, sendButton) - - if conversation.users.count == 1 { - let callButton = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) - callButton.setImage(UIImage(systemName: "phone.fill.arrow.up.right"), for: .normal) - callButton.addTarget(self, action: #selector(makeCallButtonTapped), for: .touchUpInside) - let callButtonItem = UIBarButtonItem(customView: callButton) - - navigationItem.rightBarButtonItem = callButtonItem - } - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - chatListViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - chatListViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - chatListViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - chatListViewController.view.bottomAnchor.constraint(equalTo: inputField.topAnchor, constant: -20), - - inputStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), - inputStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12), - inputStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - - inputField.heightAnchor.constraint(equalToConstant: 44), - - sendButton.widthAnchor.constraint(equalToConstant: 44), - imageButton.widthAnchor.constraint(equalToConstant: 44), - - sendButton.imageView!.widthAnchor.constraint(equalToConstant: 25), - sendButton.imageView!.heightAnchor.constraint(equalToConstant: 25), - imageButton.imageView!.widthAnchor.constraint(equalToConstant: 25), - imageButton.imageView!.heightAnchor.constraint(equalToConstant: 25), - ]) - } - - private func finishLoadingIfNeeded() { - if conversationsLoaded.apiConv == true && conversationsLoaded.nxmConv == true { - toggleLoading() - DispatchQueue.main.async { - self.chatListViewController.view.isHidden = false - } - } - } - - @objc private func keyboardWasShown(notification: NSNotification) { - if let kbSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size { - self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height - 20, right: 0) - } - } - - @objc private func makeCallButtonTapped() { - if let user = conversation.users.first { - present(CallViewController(user: user), animated: true, completion: nil) - } - } - - private func decorateConversation() { - let path = "\(Conversations.path)/\(conversation.id)" - let token = ClientManager.shared.token - RemoteLoader.load(path: path, - authToken: token, - body: Optional.none, - responseType: Conversations.Decorate.Response.self) { [weak self] result in - guard let self = self else { return } - self.conversationsLoaded.apiConv = true - switch result { - case .success(let conversation): - self.conversation = conversation - if let events = conversation.events { - self.chatListViewController.setMessages(messages: self.processEvents(events: events)) - } - - case .failure(let error): - self.showErrorAlert(message: error.localizedDescription) - } - } - } - - private func processEvents(events: [Conversations.Conversation.Event]) -> [ChatMessage] { - guard let currentUser = ClientManager.shared.user else { return [] } - - var allUsers: [(id: String, displayName: String)] = conversation.users.map { ($0.id, $0.displayName) } - allUsers.append((currentUser.id, currentUser.displayName)) - - let processedEvents: [ChatMessage] = - Array(Set(events)) - .compactMap { event in - guard let user = (allUsers.first { $0.id == event.from }), - let eventID = Int(event.id), - let eventDate = VDateFormatter.dateFor(event.timestamp) else { return nil } - - if event.type.contains("message") { - switch event.type { - case "message.text": - return ChatMessage(id: eventID, sender: user.displayName, content: .text(content: event.content!), date: eventDate) - case "message.image": - return ChatMessage(id: eventID, sender: user.displayName, content: .image(urlString: event.content!), date: eventDate) - default: - return nil - } - } else if event.type.contains("member") { - var action = event.type.split(separator: ":")[1] - - if action == "invited" { - action = "was invited" - } - let content = "\(user.displayName) \(action)." - return ChatMessage(id: eventID, sender: user.displayName, content: .info(content: content), date: eventDate) - } else { - return nil - } - } - .sorted { $0.id < $1.id } - - return processedEvents - } - - @objc private func sendMessage() { - guard let text = inputField.text else { return } - // set current state for input field - DispatchQueue.main.async { [weak self] in - self?.inputField.text = "" - self?.inputField.resignFirstResponder() - self?.inputField.isEnabled = false - } - - // send message - let message = NXMMessage(text: text) - nxmConversation?.sendMessage(message, completionHandler: { [weak self] error in - DispatchQueue.main.async { [weak self] in - self?.inputField.isEnabled = true - } - }) - } - - @objc private func pickImage() { - self.present(imagePicker, animated: true, completion: nil) - } - - private func sendImage(imageData: Data) { - ClientManager.shared.uploadImage(imageData: imageData) { error, imageURL in - if error != nil { - self.showErrorAlert(message: error?.localizedDescription) - return - } - - let message = NXMMessage(imageUrl: imageURL!) - self.nxmConversation?.sendMessage(message, completionHandler: { [weak self] error in - if error != nil { - self?.showErrorAlert(message: error?.localizedDescription) - } - }) - } - } -} - -extension ChatViewController: UITextFieldDelegate { - func textFieldDidEndEditing(_ textField: UITextField) { - self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - sendMessage() - return true - } -} - -extension ChatViewController: NXMConversationDelegate { - func conversation(_ conversation: NXMConversation, didReceive error: Error) { - showErrorAlert(message: error.localizedDescription) - } - - func conversation(_ conversation: NXMConversation, didReceive event: NXMMessageEvent) { - chatListViewController.appendMessage(event.asChatMessage()) - } -} - -extension ChatViewController: ClientManagerConversationDelegate { - func clientManager(_ clientManager: ClientManager, didGetConversation conversation: NXMConversation?) { - conversationsLoaded.nxmConv = true - self.nxmConversation = conversation - self.nxmConversation?.delegate = self - } - - func clientManager(_ clientManager: ClientManager, getConversationDidFail errorMessage: String?) { - showErrorAlert(message: errorMessage) - } -} - -extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - if let imageData = pickedImage.jpegData(compressionQuality: 0.1) { - sendImage(imageData: imageData) - } - } - dismiss(animated: true, completion: nil) - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - dismiss(animated: true, completion: nil) - } -} diff --git a/client-ios/TheApp/ViewControllers/ChatViewController.swift b/client-ios/TheApp/ViewControllers/ChatViewController.swift deleted file mode 100644 index 881776f..0000000 --- a/client-ios/TheApp/ViewControllers/ChatViewController.swift +++ /dev/null @@ -1,234 +0,0 @@ -import UIKit -import NexmoClient - -class ChatViewController: UIViewController, LoadingViewController { - - private lazy var inputField: VTextField = { - let input = VTextField(placeholder: "Type a message") - input.delegate = self - input.returnKeyType = .send - input.translatesAutoresizingMaskIntoConstraints = false - return input - }() - - private lazy var conversationTextView: UITextView = { - let textView = UITextView() - textView.backgroundColor = .gray - textView.translatesAutoresizingMaskIntoConstraints = false - textView.isEditable = false - return textView - }() - - lazy var spinnerView = SpinnerView(parentView: view) - - var nxmConversation: NXMConversation? - - private let client = NXMClient.shared - private var conversation: Conversations.Conversation - - init(conversation: Conversations.Conversation) { - self.conversation = conversation - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - - toggleLoading() - getConversation() - decorateConversation() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWasShown), - name: UIResponder.keyboardDidShowNotification, object: nil) - } - - private func setUpView() { - title = conversation.displayName - view.backgroundColor = .white - view.addSubviews(conversationTextView, inputField) - - if conversation.users.count == 1 { - let callButton = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) - callButton.setImage(UIImage(systemName: "phone.fill.arrow.up.right"), for: .normal) - callButton.addTarget(self, action: #selector(makeCallButtonTapped), for: .touchUpInside) - let callButtonItem = UIBarButtonItem(customView: callButton) - - navigationItem.rightBarButtonItem = callButtonItem - } - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - conversationTextView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - conversationTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - conversationTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - conversationTextView.bottomAnchor.constraint(equalTo: inputField.topAnchor, constant: -20), - - inputField.leadingAnchor.constraint(equalTo: view.leadingAnchor), - inputField.trailingAnchor.constraint(equalTo: view.trailingAnchor), - inputField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - inputField.heightAnchor.constraint(equalToConstant: 50) - ]) - } - - @objc func keyboardWasShown(notification: NSNotification) { - if let kbSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size { - self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height - 20, right: 0) - } - } - - @objc func makeCallButtonTapped() { - if let user = conversation.users.first { - present(CallViewController(user: user), animated: true, completion: nil) - } - } - - func getConversation() { - client.getConversationWithUuid(conversation.id) { [weak self] (error, conversation) in - self?.nxmConversation = conversation - conversation?.delegate = self - } - } - - func decorateConversation() { - let path = "\(Conversations.path)/\(conversation.id)" - let token = NXMClient.shared.authToken - - RemoteLoader.load(path: path, - authToken: token, - body: Optional.none, - responseType: Conversations.Decorate.Response.self) { [weak self] result in - guard let self = self else { return } - self.toggleLoading() - switch result { - case .success(let conversation): - self.conversation = conversation - if let events = conversation.events { - self.processEvents(events: events) - } - - case .failure(let error): - self.showErrorAlert(message: error.localizedDescription) - } - } - } - - func processEvents(events: [Conversations.Conversation.Event]) { - Array(Set(events)) - .sorted { $0.id < $1.id } - .forEach { event in - var allUsers: [(id: String, displayName: String)] = conversation.users.map { ($0.id, $0.displayName) } - allUsers.append((client.user!.uuid, client.user!.displayName)) - - guard let user = (allUsers.first { $0.id == event.from }) else { return } - - if let text = event.content, event.type == "text" { - addConversationLine("\(user.displayName) said: '\(text)'") - } else { - var action = event.type.split(separator: ":")[1] - - if action == "invited" { - action = "was invited" - } - addConversationLine("\(user.displayName) \(String(describing: action)).") - } - } - } - - func processNxmEvent(event: NXMEvent) { - if let memberEvent = event as? NXMMemberEvent { - showMemberEvent(event: memberEvent) - } - if let textEvent = event as? NXMTextEvent { - showTextEvent(event: textEvent) - } - } - - func showMemberEvent(event: NXMMemberEvent) { - guard let displayName = event.embeddedInfo?.user.displayName else { return } - switch event.state { - case .invited: - addConversationLine("\(displayName) was invited.") - case .joined: - addConversationLine("\(displayName) joined.") - case .left: - addConversationLine("\(displayName) left.") - case .unknown: - fatalError("Unknown member event state.") - @unknown default: - fatalError("Unknown member event state.") - } - } - - func showTextEvent(event: NXMTextEvent) { - if let message = event.text { - addConversationLine("\(event.embeddedInfo?.user.displayName ?? "A user") said: '\(message)'") - } - } - - func addConversationLine(_ line: String) { - DispatchQueue.main.async { - if let text = self.conversationTextView.text, text.count > 0 { - self.conversationTextView.text = "\(text)\n\(line)" - } else { - self.conversationTextView.text = line - } - } - } - - func send(message: String) { - // set current state for input field - DispatchQueue.main.async { [weak self] in - self?.inputField.text = "" - self?.inputField.resignFirstResponder() - self?.inputField.isEnabled = false - } - - // send message - nxmConversation?.sendText(message, completionHandler: { [weak self] (error) in - DispatchQueue.main.async { [weak self] in - self?.inputField.isEnabled = true - } - }) - } -} - -extension ChatViewController: UITextFieldDelegate { - func textFieldDidEndEditing(_ textField: UITextField) { - self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if let text = textField.text { - send(message: text) - } - return true - } -} - -extension ChatViewController: NXMConversationDelegate { - func conversation(_ conversation: NXMConversation, didReceive error: Error) { - showErrorAlert(message: error.localizedDescription) - } - - func conversation(_ conversation: NXMConversation, didReceive event: NXMTextEvent) { - self.processNxmEvent(event: event) - } - - func conversation(_ conversation: NXMConversation, didReceive event: NXMMemberEvent) { - self.processNxmEvent(event: event) - } - - func conversation(_ conversation: NXMConversation, didReceive event: NXMMessageStatusEvent) { - print(event) - } -} diff --git a/client-ios/TheApp/ViewControllers/ContactsViewController.swift.swift b/client-ios/TheApp/ViewControllers/ContactsViewController.swift.swift deleted file mode 100644 index b223aa9..0000000 --- a/client-ios/TheApp/ViewControllers/ContactsViewController.swift.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ContactsViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 19/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -protocol ContactsViewControllerDelegate: AnyObject { - func contactsViewControllerDelegateDidRefreshList(_ contactsViewControllerDelegate: ContactsViewController) -} - -class ContactsViewController: UIViewController { - - private lazy var listViewController: ListViewController = { - let vc = ListViewController(data: users, supportsRefresh: true) - vc.delegate = self - vc.view.translatesAutoresizingMaskIntoConstraints = false - return vc - }() - - private var users: [Users.User] - - weak var delegate: ContactsViewControllerDelegate? - - init(users: [Users.User]) { - self.users = users - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - guard let homeViewController = tabBarController as? HomeViewController else { return } - homeViewController.homeDelegate = self - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - view.addSubview(listViewController.view) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - listViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - listViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - listViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - listViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } - -} - -extension ContactsViewController: ListViewControllerDelegate { - func listViewControllerDelegate(_: ListViewController, didSelectRow data: T) where T : Hashable, T : ListViewPresentable { - if let user = data as? Users.User { - self.navigationController?.pushViewController(UserDetailViewController(user: user), animated: true) - } - } - - func listViewControllerDelegateDidRefresh(_: ListViewController) where T : Hashable, T : ListViewPresentable { - delegate?.contactsViewControllerDelegateDidRefreshList(self) - } -} - -extension ContactsViewController: HomeViewControllerDelegate { - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadUsers users: [Users.User]) { - self.users = users - self.listViewController.triggerUpdate(with: users) - } -} diff --git a/client-ios/TheApp/ViewControllers/ConversationListViewController.swift b/client-ios/TheApp/ViewControllers/ConversationListViewController.swift deleted file mode 100644 index 0c55287..0000000 --- a/client-ios/TheApp/ViewControllers/ConversationListViewController.swift +++ /dev/null @@ -1,78 +0,0 @@ -import UIKit - -protocol ConversationListViewControllerDelegate: AnyObject { - func conversationListViewControllerDelegateDidRefreshList(_ conversationListViewController: ConversationListViewController) -} - -class ConversationListViewController: UIViewController { - - private lazy var listViewController: ListViewController = { - let vc = ListViewController(data: conversations, supportsRefresh: true) - vc.delegate = self - vc.view.translatesAutoresizingMaskIntoConstraints = false - return vc - }() - - private var conversations: [Conversations.Conversation] - - weak var delegate: ConversationListViewControllerDelegate? - - init(conversations: [Conversations.Conversation]) { - self.conversations = conversations - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - guard let homeViewController = tabBarController as? HomeViewController else { return } - homeViewController.homeDelegate = self - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - view.addSubview(listViewController.view) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - listViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - listViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - listViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - listViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) - } -} - -extension ConversationListViewController: ListViewControllerDelegate { - func listViewControllerDelegate(_: ListViewController, didSelectRow data: T) where T : Hashable, T : ListViewPresentable { - if let conversation = data as? Conversations.Conversation { - navigationController?.pushViewController(ChatViewController(conversation: conversation), animated: true) - } - } - - func listViewControllerDelegateDidRefresh(_: ListViewController) where T : Hashable, T : ListViewPresentable { - delegate?.conversationListViewControllerDelegateDidRefreshList(self) - } -} - -extension ConversationListViewController: HomeViewControllerDelegate { - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didCreateConversation conversation: Conversations.Conversation, conversations: [Conversations.Conversation]) { - DispatchQueue.main.async { - self.dismiss(animated: true, completion: nil) - self.conversations = conversations - self.listViewController.triggerUpdate(with: self.conversations) - self.navigationController?.pushViewController(ChatViewController(conversation: conversation), animated: true) - } - } - - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadConversations conversations: [Conversations.Conversation]) { - self.conversations = conversations - self.listViewController.triggerUpdate(with: self.conversations) - } -} diff --git a/client-ios/TheApp/ViewControllers/CreateConversationViewController.swift b/client-ios/TheApp/ViewControllers/CreateConversationViewController.swift deleted file mode 100644 index ed84c10..0000000 --- a/client-ios/TheApp/ViewControllers/CreateConversationViewController.swift +++ /dev/null @@ -1,118 +0,0 @@ -import UIKit - -protocol CreateConversationViewControllerDelegate: AnyObject { - func createConversationViewController(_ createConversationViewController: CreateConversationViewController, - didCreateConversation conversation: Conversations.Conversation) -} - -class CreateConversationViewController: UIViewController, LoadingViewController { - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - label.textColor = Constants.primaryTextColor - label.text = "Select users to start a conversation with" - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var listViewController: ListViewController = { - let vc = ListViewController(data: users, supportsMultipleSelection: true) - vc.delegate = self - vc.view.translatesAutoresizingMaskIntoConstraints = false - return vc - }() - - private lazy var createButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Create Conversation", for: .normal) - button.setTitleColor(Constants.highlightColor, for: .normal) - button.addTarget(self, action: #selector(createButtonTapped), for: .touchUpInside) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - lazy var spinnerView = SpinnerView(parentView: view) - - weak var delegate: CreateConversationViewControllerDelegate? - - private let users: [Users.User] - private var selectedUsers: [Users.User] = [] - - init(users: [Users.User]) { - self.users = users - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - } - - private func setUpView() { - view.backgroundColor = Constants.secondaryBackgroundColor - view.addSubviews(titleLabel, listViewController.view, createButton) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -48), - - listViewController.view.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), - listViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - listViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - createButton.topAnchor.constraint(equalTo: listViewController.view.bottomAnchor, constant: 16), - createButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 96), - createButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -96), - createButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) - } - - @objc func createButtonTapped() { - guard selectedUsers.count > 0 else { - showErrorAlert(message: "No users selected") - return - } - - let body = Conversations.Create.Body(users: selectedUsers.map { $0.id }) - toggleLoading() - RemoteLoader.load(path: Conversations.path, - authToken: ClientManager.shared.token, - body: body, - responseType: Conversations.Create.Response.self) { [weak self] result in - guard let self = self else { return } - self.toggleLoading() - switch result { - case .success(let conversation): - self.delegate?.createConversationViewController(self, didCreateConversation: conversation) - case .failure(let error): - if case let RemoteLoaderError.api(apiError) = error { - self.showErrorAlert(message: apiError.detail) - } - } - } - } -} - -extension CreateConversationViewController: ListViewControllerDelegate { - func listViewControllerDelegate(_: ListViewController, didSelectRow data: T) where T: Hashable, T: ListViewPresentable { - if let user = data as? Users.User { - if selectedUsers.contains(user) { - if let existingIndex = selectedUsers.firstIndex(of: user) { - selectedUsers.remove(at: existingIndex) - } - } else { - selectedUsers.append(user) - } - } - } -} diff --git a/client-ios/TheApp/ViewControllers/HomeViewController.swift b/client-ios/TheApp/ViewControllers/HomeViewController.swift deleted file mode 100644 index 095d749..0000000 --- a/client-ios/TheApp/ViewControllers/HomeViewController.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// HomeViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 19/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit -import NexmoClient - -protocol HomeViewControllerDelegate: AnyObject { - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didCreateConversation conversation: Conversations.Conversation, conversations: [Conversations.Conversation]) - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadConversations conversations: [Conversations.Conversation]) - func homeViewControllerDelegate(_ HomeViewController: HomeViewController, didLoadUsers users: [Users.User]) -} - -class HomeViewController: UITabBarController { - - private let data: Auth.Response - private var conversations: [Conversations.Conversation] - private var users: [Users.User] - - private let conversationListViewController: ConversationListViewController - private let contactsViewController: ContactsViewController - private let settingsViewController: SettingsViewController - - private var newConversationButton: UIBarButtonItem? - - weak var homeDelegate: HomeViewControllerDelegate? - - init(data: Auth.Response) { - self.data = data - self.conversations = data.conversations - self.users = data.users - self.conversationListViewController = ConversationListViewController(conversations: conversations) - self.contactsViewController = ContactsViewController(users: users) - self.settingsViewController = SettingsViewController() - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - title = "The V app" - - newConversationButton = UIBarButtonItem( - barButtonSystemItem: .add, - target: self, - action: #selector(newConversationButtonTapped) - ) - - navigationItem.rightBarButtonItem = newConversationButton - self.navigationItem.hidesBackButton = true - - delegate = self - conversationListViewController.delegate = self - contactsViewController.delegate = self - ClientManager.shared.incomingCallDelegate = self - self.viewControllers = VTabBarItem.allCases.map { createTabBarViewControllers(for: $0) } - } - - private func createTabBarViewControllers(for vBarItem: VTabBarItem) -> UIViewController { - let item = UITabBarItem(title: vBarItem.title, image: vBarItem.image, tag: vBarItem.tag) - let viewController: UIViewController - - switch vBarItem { - case .chats : - viewController = conversationListViewController - case .contacts: - viewController = contactsViewController - case .settings: - viewController = settingsViewController - } - - viewController.tabBarItem = item - return viewController - } - - @objc private func newConversationButtonTapped() { - let createConversationViewController = CreateConversationViewController(users: data.users) - createConversationViewController.delegate = self - navigationController?.present(createConversationViewController, animated: true, completion: nil) - } - - func displayIncomingCallAlert(call: NXMCall) { - var from = "Unknown" - if let otherParty = call.allMembers.first { - from = otherParty.channel?.from.data ?? otherParty.user.name - } - let presentingVC = navigationController?.presentedViewController ?? self - - let alert = UIAlertController(title: "Incoming call from", message: from, preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "Answer", style: .default, handler: { _ in - presentingVC.present(CallViewController(call: call), animated: true, completion: nil) - })) - alert.addAction(UIAlertAction(title: "Reject", style: .destructive, handler: { _ in - call.reject(nil) - })) - - alert.view.tintColor = Constants.highlightColor - - presentingVC.present(alert, animated: true, completion: nil) - } - - private func loadConversations() { - RemoteLoader.load(path: Conversations.path, - authToken: ClientManager.shared.token, - body: Optional.none, - responseType: Conversations.List.Response.self) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let conversations): - self.conversations = conversations - self.homeDelegate?.homeViewControllerDelegate(self, didLoadConversations: conversations) - case .failure(let error): - self.showErrorAlert(message: error.localizedDescription) - } - } - } - - private func loadUsers() { - RemoteLoader.load(path: Users.path, - authToken: ClientManager.shared.token, - body: Optional.none, - responseType: Users.List.Response.self) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let users): - self.users = users - self.homeDelegate?.homeViewControllerDelegate(self, didLoadUsers: users) - case .failure(let error): - self.showErrorAlert(message: error.localizedDescription) - } - } - } -} - -extension HomeViewController: UITabBarControllerDelegate { - override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - if item.tag != VTabBarItem.chats.tag { - navigationItem.rightBarButtonItem = nil - } else { - navigationItem.rightBarButtonItem = newConversationButton - } - } -} - -extension HomeViewController: CreateConversationViewControllerDelegate { - func createConversationViewController(_ createConversationViewController: CreateConversationViewController, didCreateConversation conversation: Conversations.Conversation) { - self.conversations.append(conversation) - homeDelegate?.homeViewControllerDelegate(self, didCreateConversation: conversation, conversations: conversations) - } -} - -extension HomeViewController: ConversationListViewControllerDelegate { - func conversationListViewControllerDelegateDidRefreshList(_ conversationListViewController: ConversationListViewController) { - loadConversations() - } -} - -extension HomeViewController: ContactsViewControllerDelegate { - func contactsViewControllerDelegateDidRefreshList(_ contactsViewControllerDelegate: ContactsViewController) { - loadUsers() - } -} - -extension HomeViewController: ClientManagerIncomingCallDelegate { - func clientManager(_ clientManager: ClientManager, didReceiveCall call: NXMCall) { - DispatchQueue.main.async { - self.displayIncomingCallAlert(call: call) - } - } -} diff --git a/client-ios/TheApp/ViewControllers/ListViewController.swift b/client-ios/TheApp/ViewControllers/ListViewController.swift deleted file mode 100644 index 1e8d728..0000000 --- a/client-ios/TheApp/ViewControllers/ListViewController.swift +++ /dev/null @@ -1,141 +0,0 @@ -import UIKit - -protocol ListViewControllerDelegate: AnyObject { - func listViewControllerDelegate(_: ListViewController, didSelectRow data: T) - func listViewControllerDelegateDidRefresh(_: ListViewController) -} - -class ListViewController: UIViewController, UICollectionViewDelegate { - - private lazy var collectionView = makeCollectionView() - private lazy var dataSource = makeDataSource() - private lazy var refreshControl: UIRefreshControl = { - let rc = UIRefreshControl() - rc.tintColor = Constants.primaryTextColor - rc.addTarget(self, action: #selector(refresh), for: .valueChanged) - return rc - }() - - private var data: [T] - private let supportsMultipleSelection: Bool - private let supportsRefresh: Bool - - weak var delegate: ListViewControllerDelegate? - - init(data: [T], supportsMultipleSelection: Bool = false, supportsRefresh: Bool = false) { - self.data = data - self.supportsMultipleSelection = supportsMultipleSelection - self.supportsRefresh = supportsRefresh - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - updateList() - } - - private func setUpView() { - collectionView.dataSource = dataSource - collectionView.delegate = self - if supportsRefresh { - collectionView.addSubview(refreshControl) - } - view.addSubview(collectionView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - collectionView.topAnchor.constraint(equalTo: view.topAnchor), - collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - public func triggerUpdate(with newData: [T]) { - DispatchQueue.main.async { - self.data = newData - self.updateList() - self.refreshControl.endRefreshing() - } - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if let row = collectionView.cellForItem(at: indexPath) as? UICollectionViewListCell { - let index = indexPath.item - if supportsMultipleSelection { - row.tintColor = Constants.highlightColor - row.accessories = row.accessories.count == 0 ? [UICellAccessory.checkmark()] : [] - } - delegate?.listViewControllerDelegate(self, didSelectRow: data[index]) - } - collectionView.deselectItem(at: indexPath, animated: true) - } - - @objc func refresh() { - self.delegate?.listViewControllerDelegateDidRefresh(self) - } -} - -// Data source -private extension ListViewController { - enum Section: CaseIterable { - case main - } - - func updateList() { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(Section.allCases) - snapshot.appendItems(self.data, toSection: .main) - dataSource.apply(snapshot) - } - - func makeCollectionView() -> UICollectionView { - var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - config.backgroundColor = Constants.backgroundColor - let layout = UICollectionViewCompositionalLayout.list(using: config) - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.translatesAutoresizingMaskIntoConstraints = false - return cv - } - - func makeDataSource() -> UICollectionViewDiffableDataSource { - let cellRegistration = makeCellRegistration() - - return UICollectionViewDiffableDataSource( - collectionView: collectionView, - cellProvider: { view, indexPath, item in - view.dequeueConfiguredReusableCell( - using: cellRegistration, - for: indexPath, - item: item - ) - } - ) - } - - func makeCellRegistration() -> UICollectionView.CellRegistration { - return UICollectionView.CellRegistration { cell, indexPath, data in - var config = cell.defaultContentConfiguration() - config.text = data.displayName - config.textProperties.color = Constants.primaryTextColor - - if let setting = data as? Setting { - config.image = UIImage(systemName: setting.iconString) - config.imageProperties.tintColor = Constants.highlightColor - } - - var backgroundConfig = UIBackgroundConfiguration.listGroupedCell() - backgroundConfig.backgroundColor = Constants.secondaryBackgroundColor - - cell.contentConfiguration = config - cell.backgroundConfiguration = backgroundConfig - } - } -} diff --git a/client-ios/TheApp/ViewControllers/LoginViewController.swift b/client-ios/TheApp/ViewControllers/LoginViewController.swift deleted file mode 100644 index 8868df5..0000000 --- a/client-ios/TheApp/ViewControllers/LoginViewController.swift +++ /dev/null @@ -1,114 +0,0 @@ -import UIKit - -class LoginViewController: UIViewController, LoadingViewController { - - private let usernameField = VTextField(placeholder: "Username") - private let passwordField = VTextField(placeholder: "Password", isSecure: true) - lazy var spinnerView = SpinnerView(parentView: view) - - private lazy var loginButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Login", for: .normal) - button.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var newUserButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("New User?", for: .normal) - button.addTarget(self, action: #selector(newUserButtonTapped), for: .touchUpInside) - return button - }() - - private lazy var stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 20 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView - }() - - private var loggedin = false - - override func viewDidLoad() { - super.viewDidLoad() - title = "The V App" - setUpView() - setUpConstraints() - checkExistingTokenAndLogin() - hideKeyboardWhenTappedAround() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if loggedin { - resetView() - } - } - - private func setUpView() { - view.backgroundColor = .white - stackView.addArrangedSubviews(usernameField, passwordField, loginButton, newUserButton) - view.addSubviews(stackView) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100), - - usernameField.heightAnchor.constraint(equalToConstant: 50), - passwordField.heightAnchor.constraint(equalToConstant: 50) - ]) - } - - private func checkExistingTokenAndLogin() { - if let credentials = ClientManager.shared.getCredentials() { - ClientManager.shared.delegate = self - toggleLoading() - ClientManager.shared.auth(username: credentials.0, password: credentials.1, displayName: nil, url: Auth.loginPath, storeCredentials: false) - toggleViewVisibility(hidden: true) - spinnerView.setDetailText(text: "Welcome back \(credentials.0)") - } - } - - private func toggleViewVisibility(hidden: Bool) { - stackView.isHidden = hidden - usernameField.isHidden = hidden - passwordField.isHidden = hidden - } - - private func resetView() { - loggedin = false - toggleViewVisibility(hidden: false) - } - - @objc func loginButtonTapped() { - if let username = usernameField.text, let password = passwordField.text { - ClientManager.shared.delegate = self - toggleLoading() - ClientManager.shared.auth(username: username, password: password, displayName: nil, url: Auth.loginPath) - } else { - showErrorAlert(message: "Validation error") - } - } - - @objc func newUserButtonTapped() { - let signUpViewController = SignUpViewController() - self.navigationController?.pushViewController(signUpViewController, animated: true) - } -} - -extension LoginViewController: ClientManagerDelegate { - func clientManager(_ clientManager: ClientManager, responseForAuth response: Auth.Response) { - toggleLoading() - loggedin = true - navigationController?.pushViewController(HomeViewController(data: response), animated: true) - } - - func clientManager(_ clientManager: ClientManager, authDidFail errorMessage: String?) { - toggleLoading() - showErrorAlert(message: errorMessage) - } -} diff --git a/client-ios/TheApp/ViewControllers/SettingsViewController.swift.swift b/client-ios/TheApp/ViewControllers/SettingsViewController.swift.swift deleted file mode 100644 index bfaf45b..0000000 --- a/client-ios/TheApp/ViewControllers/SettingsViewController.swift.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// SettingsViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 19/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class SettingsViewController: UIViewController, LoadingViewController { - - private static let settings = [ - Setting( - id: "1", - displayName: "Change profile picture", - type: .picture, - iconString: "person.crop.square.fill" - ), - Setting( - id: "2", - displayName: "Log out", - type: .logout, - iconString: "person.fill.badge.minus" - ) - ] - - private lazy var profilePicView: VProfilePictureView = { - let imageView = VProfilePictureView() - imageView.imageURL = imageURL - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var nameLabel: UILabel = { - let label = UILabel() - label.text = "Logged in as \(username)" - label.textAlignment = .center - label.font = label.font.withSize(24) - label.numberOfLines = 0 - label.textColor = Constants.primaryTextColor - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var listViewController: ListViewController = { - let vc = ListViewController(data: SettingsViewController.settings, supportsMultipleSelection: false) - vc.delegate = self - vc.view.translatesAutoresizingMaskIntoConstraints = false - return vc - }() - - private lazy var imagePicker: UIImagePickerController = { - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.allowsEditing = false - imagePicker.sourceType = .photoLibrary - return imagePicker - }() - - lazy var spinnerView = SpinnerView(parentView: view) - - private let username: String = ClientManager.shared.user?.displayName ?? "" - private let imageURL = ClientManager.shared.user?.imageURL - private let token = ClientManager.shared.token - - private var image: UIImage? - - init() { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - view.addSubviews(profilePicView, nameLabel, listViewController.view) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - profilePicView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 48), - profilePicView.widthAnchor.constraint(equalToConstant: 200), - profilePicView.heightAnchor.constraint(equalToConstant: 200), - profilePicView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - nameLabel.topAnchor.constraint(equalTo: profilePicView.bottomAnchor, constant: 16), - nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - listViewController.view.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 48), - listViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - listViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - listViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } -} - -extension SettingsViewController: ListViewControllerDelegate { - func listViewControllerDelegate(_: ListViewController, didSelectRow data: T) where T : Hashable, T : ListViewPresentable { - if let setting = data as? Setting { - switch setting.type { - case .picture: - self.present(imagePicker, animated: true, completion: nil) - case .logout: - ClientManager.shared.logout() - self.navigationController?.popViewController(animated: true) - } - } - } -} - -extension SettingsViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - if let imageData = pickedImage.jpegData(compressionQuality: 0.1) { - toggleLoading() - spinnerView.setDetailText(text: "Uploading your image") - ClientManager.shared.uploadImage(imageData: imageData) { error, imageUrl in - if let imageUrl = imageUrl { - RemoteLoader.load( - path: Image.path, - authToken: ClientManager.shared.token, - body: Image.Body(imageURL: imageUrl), responseType: Image.Body.self) { result in - DispatchQueue.main.async { - self.toggleLoading() - switch result { - case .success: - self.profilePicView.image = pickedImage - case .failure(let error): - self.showErrorAlert(message: error.localizedDescription) - } - } - } - } else { - self.showErrorAlert(message: error?.localizedDescription ?? "") - } - } - } - } - dismiss(animated: true, completion: nil) - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - dismiss(animated: true, completion: nil) - } -} diff --git a/client-ios/TheApp/ViewControllers/UserDetailViewController.swift b/client-ios/TheApp/ViewControllers/UserDetailViewController.swift deleted file mode 100644 index 1feaf0e..0000000 --- a/client-ios/TheApp/ViewControllers/UserDetailViewController.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// UserDetailViewController.swift -// TheApp -// -// Created by Abdulhakim Ajetunmobi on 22/07/2021. -// Copyright © 2021 Vonage. All rights reserved. -// - -import UIKit - -class UserDetailViewController: UIViewController { - - private lazy var profilePicView: VProfilePictureView = { - let imageView = VProfilePictureView() - imageView.imageURL = user.imageURL - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var nameLabel: UILabel = { - let label = UILabel() - label.text = user.displayName - label.textAlignment = .center - label.textColor = Constants.primaryTextColor - label.font = label.font.withSize(24) - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var callButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Call", for: .normal) - button.setTitleColor(Constants.highlightColor, for: .normal) - button.titleLabel?.font = button.titleLabel?.font.withSize(48) - button.addTarget(self, action: #selector(callButtonTapped), for: .touchUpInside) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - private let user: Users.User - - init(user: Users.User) { - self.user = user - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setUpView() - setUpConstraints() - } - - private func setUpView() { - view.backgroundColor = Constants.backgroundColor - view.addSubviews(profilePicView, nameLabel, callButton) - } - - private func setUpConstraints() { - NSLayoutConstraint.activate([ - profilePicView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 48), - profilePicView.widthAnchor.constraint(equalToConstant: 200), - profilePicView.heightAnchor.constraint(equalToConstant: 200), - profilePicView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - nameLabel.topAnchor.constraint(equalTo: profilePicView.bottomAnchor, constant: 16), - nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - callButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), - callButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - ]) - } - - @objc func callButtonTapped() { - present(CallViewController(user: user), animated: true, completion: nil) - } -} diff --git a/client-ios/Vapp.xcodeproj/project.pbxproj b/client-ios/Vapp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4e82e39 --- /dev/null +++ b/client-ios/Vapp.xcodeproj/project.pbxproj @@ -0,0 +1,462 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 12050C9B2BB1C6EE00F4893E /* IncomingChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12050C9A2BB1C6EE00F4893E /* IncomingChatView.swift */; }; + 12050CA02BB451F200F4893E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12050C9F2BB451F200F4893E /* SettingsView.swift */; }; + 12050CA32BB59AA900F4893E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12050CA22BB59AA900F4893E /* HomeViewModel.swift */; }; + 1207FD232BA08F4C009650EE /* Vapp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD222BA08F4C009650EE /* Vapp.swift */; }; + 1207FD252BA08F4C009650EE /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD242BA08F4C009650EE /* SignUpView.swift */; }; + 1207FD272BA08F4E009650EE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1207FD262BA08F4E009650EE /* Assets.xcassets */; }; + 1207FD2A2BA08F4E009650EE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1207FD292BA08F4E009650EE /* Preview Assets.xcassets */; }; + 1207FD312BA092C7009650EE /* RemoteLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD302BA092C7009650EE /* RemoteLoader.swift */; }; + 1207FD332BA092FC009650EE /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD322BA092FC009650EE /* Models.swift */; }; + 1207FD352BA09B3C009650EE /* LogInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD342BA09B3C009650EE /* LogInView.swift */; }; + 1207FD372BA0A2BE009650EE /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD362BA0A2BE009650EE /* HomeView.swift */; }; + 1207FD3A2BA0A2FA009650EE /* ClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1207FD392BA0A2FA009650EE /* ClientManager.swift */; }; + 120F4CEE2BADAD040087B0CC /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120F4CED2BADAD040087B0CC /* CallView.swift */; }; + 120F4CF02BADE12B0087B0CC /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120F4CEF2BADE12B0087B0CC /* IncomingCallView.swift */; }; + 129727842BA85C97005F60A3 /* CreateConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 129727832BA85C97005F60A3 /* CreateConversationView.swift */; }; + 129727872BA894C8005F60A3 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 129727862BA894C8005F60A3 /* ChatView.swift */; }; + 129727892BA9A0A5005F60A3 /* ChatInviteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 129727882BA9A0A5005F60A3 /* ChatInviteView.swift */; }; + 1297278B2BA9C7A8005F60A3 /* UsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1297278A2BA9C7A8005F60A3 /* UsersView.swift */; }; + 12E68B1F2BA1BC98008184F5 /* VonageClientSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 12E68B1E2BA1BC98008184F5 /* VonageClientSDK */; }; + 12E68B212BA1BE71008184F5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E68B202BA1BE71008184F5 /* Constants.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 12050C9A2BB1C6EE00F4893E /* IncomingChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingChatView.swift; sourceTree = ""; }; + 12050C9F2BB451F200F4893E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 12050CA22BB59AA900F4893E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; + 1207FD1F2BA08F4C009650EE /* Vapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1207FD222BA08F4C009650EE /* Vapp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vapp.swift; sourceTree = ""; }; + 1207FD242BA08F4C009650EE /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; + 1207FD262BA08F4E009650EE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1207FD292BA08F4E009650EE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 1207FD302BA092C7009650EE /* RemoteLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteLoader.swift; sourceTree = ""; }; + 1207FD322BA092FC009650EE /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 1207FD342BA09B3C009650EE /* LogInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogInView.swift; sourceTree = ""; }; + 1207FD362BA0A2BE009650EE /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 1207FD392BA0A2FA009650EE /* ClientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientManager.swift; sourceTree = ""; }; + 120F4CED2BADAD040087B0CC /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; + 120F4CEF2BADE12B0087B0CC /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; + 129727832BA85C97005F60A3 /* CreateConversationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateConversationView.swift; sourceTree = ""; }; + 129727862BA894C8005F60A3 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; + 129727882BA9A0A5005F60A3 /* ChatInviteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInviteView.swift; sourceTree = ""; }; + 1297278A2BA9C7A8005F60A3 /* UsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersView.swift; sourceTree = ""; }; + 12E68B202BA1BE71008184F5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1207FD1C2BA08F4C009650EE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 12E68B1F2BA1BC98008184F5 /* VonageClientSDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 12050CA12BB59A9600F4893E /* Home */ = { + isa = PBXGroup; + children = ( + 1207FD362BA0A2BE009650EE /* HomeView.swift */, + 12050CA22BB59AA900F4893E /* HomeViewModel.swift */, + ); + path = Home; + sourceTree = ""; + }; + 1207FD162BA08F4C009650EE = { + isa = PBXGroup; + children = ( + 1207FD212BA08F4C009650EE /* Vapp */, + 1207FD202BA08F4C009650EE /* Products */, + ); + sourceTree = ""; + }; + 1207FD202BA08F4C009650EE /* Products */ = { + isa = PBXGroup; + children = ( + 1207FD1F2BA08F4C009650EE /* Vapp.app */, + ); + name = Products; + sourceTree = ""; + }; + 1207FD212BA08F4C009650EE /* Vapp */ = { + isa = PBXGroup; + children = ( + 1207FD222BA08F4C009650EE /* Vapp.swift */, + 120F4CED2BADAD040087B0CC /* CallView.swift */, + 129727862BA894C8005F60A3 /* ChatView.swift */, + 1297278A2BA9C7A8005F60A3 /* UsersView.swift */, + 12050C9F2BB451F200F4893E /* SettingsView.swift */, + 129727882BA9A0A5005F60A3 /* ChatInviteView.swift */, + 120F4CEF2BADE12B0087B0CC /* IncomingCallView.swift */, + 12050C9A2BB1C6EE00F4893E /* IncomingChatView.swift */, + 129727832BA85C97005F60A3 /* CreateConversationView.swift */, + 12050CA12BB59A9600F4893E /* Home */, + 129727852BA85CA6005F60A3 /* Auth */, + 1207FD382BA0A2CD009650EE /* Helpers */, + 1207FD262BA08F4E009650EE /* Assets.xcassets */, + 1207FD282BA08F4E009650EE /* Preview Content */, + ); + path = Vapp; + sourceTree = ""; + }; + 1207FD282BA08F4E009650EE /* Preview Content */ = { + isa = PBXGroup; + children = ( + 1207FD292BA08F4E009650EE /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 1207FD382BA0A2CD009650EE /* Helpers */ = { + isa = PBXGroup; + children = ( + 1207FD322BA092FC009650EE /* Models.swift */, + 1207FD302BA092C7009650EE /* RemoteLoader.swift */, + 1207FD392BA0A2FA009650EE /* ClientManager.swift */, + 12E68B202BA1BE71008184F5 /* Constants.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 129727852BA85CA6005F60A3 /* Auth */ = { + isa = PBXGroup; + children = ( + 1207FD342BA09B3C009650EE /* LogInView.swift */, + 1207FD242BA08F4C009650EE /* SignUpView.swift */, + ); + path = Auth; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1207FD1E2BA08F4C009650EE /* Vapp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1207FD2D2BA08F4E009650EE /* Build configuration list for PBXNativeTarget "Vapp" */; + buildPhases = ( + 1207FD1B2BA08F4C009650EE /* Sources */, + 1207FD1C2BA08F4C009650EE /* Frameworks */, + 1207FD1D2BA08F4C009650EE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Vapp; + packageProductDependencies = ( + 12E68B1E2BA1BC98008184F5 /* VonageClientSDK */, + ); + productName = Vapp; + productReference = 1207FD1F2BA08F4C009650EE /* Vapp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1207FD172BA08F4C009650EE /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; + TargetAttributes = { + 1207FD1E2BA08F4C009650EE = { + CreatedOnToolsVersion = 15.2; + }; + }; + }; + buildConfigurationList = 1207FD1A2BA08F4C009650EE /* Build configuration list for PBXProject "Vapp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1207FD162BA08F4C009650EE; + packageReferences = ( + 12E68B1D2BA1BC98008184F5 /* XCRemoteSwiftPackageReference "vonage-client-sdk-ios" */, + ); + productRefGroup = 1207FD202BA08F4C009650EE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1207FD1E2BA08F4C009650EE /* Vapp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1207FD1D2BA08F4C009650EE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1207FD2A2BA08F4E009650EE /* Preview Assets.xcassets in Resources */, + 1207FD272BA08F4E009650EE /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1207FD1B2BA08F4C009650EE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 12050CA32BB59AA900F4893E /* HomeViewModel.swift in Sources */, + 129727842BA85C97005F60A3 /* CreateConversationView.swift in Sources */, + 129727892BA9A0A5005F60A3 /* ChatInviteView.swift in Sources */, + 1207FD352BA09B3C009650EE /* LogInView.swift in Sources */, + 1207FD252BA08F4C009650EE /* SignUpView.swift in Sources */, + 120F4CEE2BADAD040087B0CC /* CallView.swift in Sources */, + 1207FD3A2BA0A2FA009650EE /* ClientManager.swift in Sources */, + 1207FD312BA092C7009650EE /* RemoteLoader.swift in Sources */, + 1207FD372BA0A2BE009650EE /* HomeView.swift in Sources */, + 1207FD232BA08F4C009650EE /* Vapp.swift in Sources */, + 12050CA02BB451F200F4893E /* SettingsView.swift in Sources */, + 1207FD332BA092FC009650EE /* Models.swift in Sources */, + 12050C9B2BB1C6EE00F4893E /* IncomingChatView.swift in Sources */, + 120F4CF02BADE12B0087B0CC /* IncomingCallView.swift in Sources */, + 12E68B212BA1BE71008184F5 /* Constants.swift in Sources */, + 129727872BA894C8005F60A3 /* ChatView.swift in Sources */, + 1297278B2BA9C7A8005F60A3 /* UsersView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1207FD2B2BA08F4E009650EE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1207FD2C2BA08F4E009650EE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1207FD2E2BA08F4E009650EE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Vapp/Preview Content\""; + DEVELOPMENT_TEAM = 7F2B5ZSP8Q; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = com.vonage.Vapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1207FD2F2BA08F4E009650EE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Vapp/Preview Content\""; + DEVELOPMENT_TEAM = 7F2B5ZSP8Q; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = com.vonage.Vapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1207FD1A2BA08F4C009650EE /* Build configuration list for PBXProject "Vapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1207FD2B2BA08F4E009650EE /* Debug */, + 1207FD2C2BA08F4E009650EE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1207FD2D2BA08F4E009650EE /* Build configuration list for PBXNativeTarget "Vapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1207FD2E2BA08F4E009650EE /* Debug */, + 1207FD2F2BA08F4E009650EE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 12E68B1D2BA1BC98008184F5 /* XCRemoteSwiftPackageReference "vonage-client-sdk-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Vonage/vonage-client-sdk-ios"; + requirement = { + kind = exactVersion; + version = 1.5.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 12E68B1E2BA1BC98008184F5 /* VonageClientSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 12E68B1D2BA1BC98008184F5 /* XCRemoteSwiftPackageReference "vonage-client-sdk-ios" */; + productName = VonageClientSDK; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1207FD172BA08F4C009650EE /* Project object */; +} diff --git a/client-ios/Vapp.xcodeproj/xcshareddata/xcschemes/Vapp.xcscheme b/client-ios/Vapp.xcodeproj/xcshareddata/xcschemes/Vapp.xcscheme new file mode 100644 index 0000000..57c0a1b --- /dev/null +++ b/client-ios/Vapp.xcodeproj/xcshareddata/xcschemes/Vapp.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client-ios/Vapp/Assets.xcassets/AccentColor.colorset/Contents.json b/client-ios/Vapp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/client-ios/Vapp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/100.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/100.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/100.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/1024.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/1024.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/114.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/114.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/114.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/120.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/120.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/144.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/144.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/144.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/152.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/152.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/167.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/167.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/180.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/180.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/20.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/20.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/29.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/29.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/40.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/40.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/50.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/50.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/50.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/57.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/57.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/57.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/58.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/58.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/60.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/60.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/72.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/72.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/72.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/76.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/76.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/80.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/80.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/87.png b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from client-ios/TheApp/Assets.xcassets/AppIcon.appiconset/87.png rename to client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/Contents.json b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4fdf882 --- /dev/null +++ b/client-ios/Vapp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/client-ios/TheApp/Assets.xcassets/Contents.json b/client-ios/Vapp/Assets.xcassets/Contents.json similarity index 100% rename from client-ios/TheApp/Assets.xcassets/Contents.json rename to client-ios/Vapp/Assets.xcassets/Contents.json diff --git a/client-ios/Vapp/Auth/LogInView.swift b/client-ios/Vapp/Auth/LogInView.swift new file mode 100644 index 0000000..ee5599e --- /dev/null +++ b/client-ios/Vapp/Auth/LogInView.swift @@ -0,0 +1,78 @@ +// +// LogInView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import SwiftUI + +struct LogInView: View { + @StateObject var viewModel = LogInViewModel() + @StateObject var clientManager = ClientManager.shared + + var body: some View { + NavigationStack { + ZStack { + if viewModel.isLoading { + ProgressView() + } else { + VStack { + TextField("Username", text: $viewModel.username).textFieldStyle(.roundedBorder) + SecureField("Password", text: $viewModel.password).textFieldStyle(.roundedBorder) + + Button("Log In") { + Task { + viewModel.isLoading = true + await viewModel.logIn() + viewModel.isLoading = false + } + }.buttonStyle(.bordered) + + Button("Sign up?") { + viewModel.showSignUp = true + }.buttonStyle(.bordered) + }.padding() + } + } + .task { + viewModel.isLoading = true + await viewModel.attemptStoredLogIn() + viewModel.isLoading = false + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) + } + .navigationDestination(isPresented: $clientManager.isAuthed) { + HomeView() + } + .navigationDestination(isPresented: $viewModel.showSignUp) { + SignUpView() + } + } + } +} + +final class LogInViewModel: ObservableObject { + @Published var username = "" + @Published var password = "" + + @Published var isLoading = false + @Published var showSignUp = false + @Published var errorContainer = (hasError: false, text: "") + + @MainActor + func logIn() async { + do { + try await ClientManager.shared.auth(username: username, password: password, path: Auth.loginPath) + username = "" + password = "" + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + func attemptStoredLogIn() async { + await ClientManager.shared.attemptStoredLogIn() + } +} diff --git a/client-ios/Vapp/Auth/SignUpView.swift b/client-ios/Vapp/Auth/SignUpView.swift new file mode 100644 index 0000000..89157a1 --- /dev/null +++ b/client-ios/Vapp/Auth/SignUpView.swift @@ -0,0 +1,63 @@ +// +// SignUpView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import SwiftUI + +struct SignUpView: View { + @StateObject var viewModel = SignUpViewModel() + @StateObject var clientManager = ClientManager.shared + + var body: some View { + NavigationStack { + ZStack { + if viewModel.isLoading { + ProgressView() + } else { + VStack { + TextField("Display Name", text: $viewModel.displayName).textFieldStyle(.roundedBorder) + TextField("Username", text: $viewModel.username).textFieldStyle(.roundedBorder) + SecureField("Password", text: $viewModel.password).textFieldStyle(.roundedBorder) + + Button("Sign Up") { + Task { + viewModel.isLoading = true + await viewModel.signUp() + viewModel.isLoading = false + } + }.buttonStyle(.bordered) + }.padding() + } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) + } + .navigationDestination(isPresented: $clientManager.isAuthed) { + HomeView() + } + } + } +} + +final class SignUpViewModel: ObservableObject { + @Published var displayName = "" + @Published var username = "" + @Published var password = "" + + @Published var isLoading = false + @Published var errorContainer = (hasError: false, text: "") + + func signUp() async { + do { + try await ClientManager.shared.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath) + username = "" + password = "" + displayName = "" + } catch { + errorContainer = (true, error.localizedDescription) + } + } +} diff --git a/client-ios/Vapp/CallView.swift b/client-ios/Vapp/CallView.swift new file mode 100644 index 0000000..b454d99 --- /dev/null +++ b/client-ios/Vapp/CallView.swift @@ -0,0 +1,151 @@ +// +// CallView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 22/03/2024. +// + +import Combine +import SwiftUI +import VonageClientSDK + +struct CallView: View { + @StateObject var viewModel: CallViewModel + + var body: some View { + + NavigationStack { + VStack { + Text(viewModel.callStatus) + .padding(16) + Text(viewModel.callTime) + .padding(16) + if viewModel.callActive { + HStack { + Button("Hang Up") { + Task { + await viewModel.hangup() + } + } + .tint(.red) + .buttonStyle(.bordered) + + Button(viewModel.muteButtonText) { + Task { + await viewModel.toggleMute() + } + }.buttonStyle(.bordered) + } + } + } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) + } + .navigationTitle(viewModel.callee) + .navigationBarTitleDisplayMode(.inline) + } +} + +final class CallViewModel: ObservableObject { + private let clientManager = ClientManager.shared + private let formatter = DateComponentsFormatter() + private var subscriptions = Set() + + private var callTimer: Timer? + private var callCounter = 0 + + private let callId: String + let callee: String + + @Published var muteButtonText = "Mute" + @Published var callStatus = "" + @Published var callTime = "" + @Published var callActive = true + + @Published var errorContainer = (hasError: false, text: "") + + init(callId: String, callee: String) { + self.callId = callId + self.callee = callee + + formatter.allowedUnits = [.minute, .second] + formatter.zeroFormattingBehavior = .pad + + callTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(incrementTimer), userInfo: nil, repeats: true) + + clientManager.onCallEvent + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + switch event { + case .hangup(let callId, let reason): + guard callId == self?.callId else { return } + self?.callActive = false + self?.resetTimer() + switch reason { + case .remoteHangup: + self?.callStatus = "\(callee) Hung Up" + case .remoteReject: + self?.callStatus = "\(callee) Rejected" + case .remoteNoAnswerTimeout: + self?.callStatus = "\(callee) Did not answer" + case .localHangup: + self?.callStatus = "Call Ended" + default: + break + } + case .update(let callId, _, let status): + guard callId == self?.callId else { return } + switch status { + case .ringing: + self?.callStatus = "Ringing" + case .answered: + self?.callStatus = "On Call" + case .completed: + self?.callStatus = "Call Completed" + case .unknown: + break + @unknown default: + break + } + } + }.store(in: &subscriptions) + } + + @MainActor + @objc func incrementTimer() { + callCounter += 1 + callTime = formatter.string(from: TimeInterval(callCounter)) ?? "" + } + + @MainActor + func hangup() async { + do { + try await clientManager.client.hangup(callId) + callActive = false + resetTimer() + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func toggleMute() async { + do { + if muteButtonText == "Mute" { + try await clientManager.client.mute(callId) + muteButtonText = "Unmute" + } else { + try await clientManager.client.unmute(callId) + muteButtonText = "Mute" + } + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + private func resetTimer() { + callTimer?.invalidate() + callTimer = nil + } +} diff --git a/client-ios/Vapp/ChatInviteView.swift b/client-ios/Vapp/ChatInviteView.swift new file mode 100644 index 0000000..dfb59d3 --- /dev/null +++ b/client-ios/Vapp/ChatInviteView.swift @@ -0,0 +1,81 @@ +// +// ChatInviteView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 19/03/2024. +// + +import SwiftUI +import VonageClientSDK + +struct ChatInviteView: View { + @StateObject var viewModel: ChatInviteViewModel + @State private var selectedUsers = Set() + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + if viewModel.isLoading { + ProgressView() + } else { + VStack { + List(viewModel.users, selection: $selectedUsers) { user in + Text(user.name) + }.environment(\.editMode, .constant(EditMode.active)) + + Spacer() + + Button("Invite") { + Task { + viewModel.isLoading = true + await viewModel.invite(selectedUsers) + viewModel.isLoading = false + } + }.buttonStyle(.bordered) + } + } + } + .onChange(of: viewModel.membersInvited, initial: false) { oldValue, newValue in + if newValue { dismiss() } + } + .navigationTitle("Invite to Conversation") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +final class ChatInviteViewModel: ObservableObject { + private let conversationId: String + private let clientManager = ClientManager.shared + + var users: [Users.User] { + clientManager.users + } + + @Published var isLoading = false + @Published var membersInvited = false + + init(conversationId: String) { + self.conversationId = conversationId + } + + public func invite(_ invitedUsers: Set) async { + let usernames = invitedUsers.map { uId in + let user = users.first { $0.id == uId } + return user?.name + }.compactMap { $0 } + + await withThrowingTaskGroup(of: Void.self) { group in + for username in usernames { + group.addTask { + let memberId = try await self.clientManager.client.inviteToConversation(self.conversationId, username: username) + } + } + } + + await MainActor.run { + membersInvited = true + } + } +} diff --git a/client-ios/Vapp/ChatView.swift b/client-ios/Vapp/ChatView.swift new file mode 100644 index 0000000..f2ef410 --- /dev/null +++ b/client-ios/Vapp/ChatView.swift @@ -0,0 +1,230 @@ +// +// ChatView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 18/03/2024. +// + +import Combine +import SwiftUI +import VonageClientSDK + +struct ChatView: View { + @StateObject var viewModel: ChatViewModel + @State private var message: String = "" + @State private var listTopId: Int? + + var body: some View { + NavigationStack { + ZStack { + if viewModel.events.isEmpty || viewModel.isLoading { + ProgressView() + } else { + VStack { + ScrollViewReader { proxy in + List { + ForEach(viewModel.events.reversed(), id: \.id) { event in + switch event.kind { + case .memberJoined, .memberLeft, .memberInvited: + let displayText = viewModel.generateDisplayText(event) + Text(displayText.body) + .frame(maxWidth: .infinity, alignment: .center) + case.messageText: + let displayText = viewModel.generateDisplayText(event) + Text(displayText.body) + .frame(maxWidth: .infinity, alignment: displayText.isUser ? .trailing : .leading) + default: + EmptyView() + } + }.listRowSeparator(.hidden) + } + .onAppear { + proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom) + } + .listStyle(.plain) + .refreshable { + Task { + await viewModel.loadEarlierEvents() + await MainActor.run { + proxy.scrollTo(viewModel.cursorSize, anchor: .top) + } + } + } + + Spacer(minLength: 16) + + HStack { + TextField("Message", text: $message) + Button("Send") { + Task { + await viewModel.sendMessage(message) + self.message = "" + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + withAnimation(.easeInOut(duration: 1)) { + proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom) + } + } + } + }.buttonStyle(.bordered) + }.padding(8) + } + } + } + } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) + } + .navigationTitle(viewModel.conversationDisplayName) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + viewModel.showInviteUser = true + }) { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $viewModel.showInviteUser) { + ChatInviteView(viewModel: .init(conversationId: viewModel.conversationId)) + } + .task { + viewModel.isLoading = true + await viewModel.getMemberIdIfNeeded() + await viewModel.getInitialConversationEvents() + viewModel.isLoading = false + } + } +} + +final class ChatViewModel: ObservableObject { + + private var memberId: String? + private var cursor: String? = nil + private let clientManager = ClientManager.shared + private var subscriptions = Set() + + @Published var events: [VGPersistentConversationEvent] = [] + @Published var showInviteUser = false + @Published var isLoading = false + + @Published var errorContainer = (hasError: false, text: "") + + let conversationId: String + let conversationDisplayName: String + let cursorSize = 20 + + init(conversationId: String, conversationDisplayName: String?) { + self.conversationId = conversationId + self.conversationDisplayName = conversationDisplayName ?? "Chat" + + clientManager.onEvent + .receive(on: DispatchQueue.main) + .map { $0 as! VGPersistentConversationEvent } + .sink { [weak self] event in + self?.events.insert(event, at: 0) + }.store(in: &subscriptions) + } + + + // MARK: - Public + + func getMemberIdIfNeeded() async { + guard memberId == nil else { return } + await getMemberId() + } + + func getInitialConversationEvents() async { + let initialEvents = await getEvents() + await MainActor.run { + self.events = initialEvents + } + } + + private func getEvents() async -> [VGPersistentConversationEvent] { + do { + let params = VGGetConversationEventsParameters(order: .desc, pageSize: cursorSize, cursor: cursor) + let eventsPage = try await clientManager.client.getConversationEvents(conversationId, parameters: params) + cursor = eventsPage.nextCursor + return eventsPage.events + } catch { + await MainActor.run { + errorContainer = (true, error.localizedDescription) + } + } + + return [] + } + + func loadEarlierEvents() async { + let earlierEvents = await getEvents() + + await MainActor.run { + self.events += earlierEvents + } + } + + func sendMessage(_ message: String) async { + guard !message.isEmpty else { return } + + do { + _ = try await clientManager.client.sendMessageTextEvent(conversationId, text: message) + } catch { + await MainActor.run { + errorContainer = (true, error.localizedDescription) + } + } + } + + func generateDisplayText(_ event: VGPersistentConversationEvent) -> (body: String, isUser: Bool) { + var from = "System" + + switch event.kind { + case .memberInvited: + let memberInvitedEvent = event as! VGMemberInvitedEvent + from = memberInvitedEvent.body.user.name + return ("\(from) Invited", false) + case .memberJoined: + let memberJoinedEvent = event as! VGMemberJoinedEvent + from = memberJoinedEvent.body.user.name + return ("\(from) joined", false) + case .memberLeft: + let memberLeftEvent = event as! VGMemberLeftEvent + from = memberLeftEvent.body.user.name + return ("\(from) left", false) + case .messageText: + let messageTextEvent = event as! VGMessageTextEvent + var isUser = false + + if let userInfo = messageTextEvent.from as? VGEmbeddedInfo { + isUser = userInfo.memberId == memberId + from = isUser ? "" : "\(userInfo.user.name): " + } + + return ("\(from) \(messageTextEvent.body.text)", isUser) + default: + return ("", false) + } + } + + // MARK: - Private + + private func getMemberId() async { + do { + let member = try await clientManager.client.getConversationMember(conversationId, memberId: "me") + + if member.state == .joined { + memberId = member.id + return + } + + memberId = try await clientManager.client.joinConversation(conversationId) + } catch { + await MainActor.run { + errorContainer = (true, error.localizedDescription) + } + } + } +} diff --git a/client-ios/Vapp/CreateConversationView.swift b/client-ios/Vapp/CreateConversationView.swift new file mode 100644 index 0000000..15e7e91 --- /dev/null +++ b/client-ios/Vapp/CreateConversationView.swift @@ -0,0 +1,77 @@ +// +// CreateConversationView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 18/03/2024. +// + +import SwiftUI +import VonageClientSDK + +struct CreateConversationView: View { + @StateObject var viewModel = NewConversationViewModel() + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + if viewModel.isLoading { + ProgressView() + } else { + Form { + Section(header: Text("Conversation Details")) { + TextField("Name", text: $viewModel.name) + TextField("Display Name", text: $viewModel.displayName) + } + + Button("Create") { + Task { + viewModel.isLoading = true + await viewModel.createConversation() + viewModel.isLoading = false + } + } + } + } + } + .onChange(of: viewModel.conversationCreated, initial: false) { oldValue, newValue in + if newValue { dismiss() } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error creating conversation"), message: Text(viewModel.errorContainer.text)) + } + .navigationTitle("Create a New Conversation") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +final class NewConversationViewModel: ObservableObject { + @Published var isLoading = false + @Published var conversationCreated = false + + @Published var name = "" + @Published var displayName = "" + + @Published var errorContainer = (hasError: false, text: "") + + private let clientManager = ClientManager.shared + + @MainActor + func createConversation() async { + if (name.isEmpty || displayName.isEmpty) { + errorContainer = (true, "Please provide a conversation name and display name.") + return + } + + let params = VGCreateConversationParameters(name: name, displayName: displayName) + + do { + let convId = try await clientManager.client.createConversation(params) + _ = try await clientManager.client.joinConversation(convId) + conversationCreated = true + } catch { + errorContainer = (true, error.localizedDescription) + } + } +} diff --git a/client-ios/Vapp/Helpers/ClientManager.swift b/client-ios/Vapp/Helpers/ClientManager.swift new file mode 100644 index 0000000..b1accc0 --- /dev/null +++ b/client-ios/Vapp/Helpers/ClientManager.swift @@ -0,0 +1,191 @@ +// +// ClientManager.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import Combine +import Foundation +import VonageClientSDK + +final class ClientManager: NSObject, ObservableObject { + static let shared = ClientManager() + + public var token: String? = nil + public var username = "" + public var client: VGVonageClient! + + @Published var isAuthed = false + var users: [Users.User] = [] + + // Chat Publisher + private var handledEventKinds: Set = [.memberInvited, .memberJoined, .memberLeft, .messageText] + private let messageSubject = PassthroughSubject() + public var onEvent: AnyPublisher { + messageSubject + .filter { self.handledEventKinds.contains($0.kind) } + .eraseToAnyPublisher() + } + + // Call Event Publisher + enum CallEvent { + case hangup(callId: String, reason: VGHangupReason) + case update(callId: String, legId: String, status: VGLegStatus) + } + private let callEventSubject = PassthroughSubject() + public var onCallEvent: AnyPublisher { + callEventSubject.eraseToAnyPublisher() + } + + // Call Publisher + enum IncomingCallEvent { + case invite(callId: String, caller: String) + case inviteCancel(callId: String, reason: VGVoiceInviteCancelReason) + } + private let incomingCallSubject = PassthroughSubject() + public var onCall: AnyPublisher { + incomingCallSubject.eraseToAnyPublisher() + } + + override init() { + super.init() + initializeClient() + } + + // MARK: - Public + + public func auth(username: String, password: String, displayName: String? = nil, path: String, shouldStoreCredentials: Bool = true) async throws { + let body = Auth.Body(name: username, password: password, displayName: displayName) + + let authResponse: Auth.Response = try await RemoteLoader.post(path: path, body: body) + self.token = authResponse.token + self.username = username + try await client?.createSession(authResponse.token) + + if shouldStoreCredentials { + storeCredentials(username: username, password: password) + } + + await MainActor.run { + isAuthed = true + users = authResponse.users + } + } + + public func attemptStoredLogIn() async { + if let credentials = getCredentials() { + try? await auth(username: credentials.0, password: credentials.1, path: Auth.loginPath, shouldStoreCredentials: false) + } + } + + public func logout() async throws { + try await client.deleteSession() + deleteCredentials(username: username) + } + + // MARK: - Private + + private func initializeClient() { + VGVonageClient.isUsingCallKit = false + let config = VGClientInitConfig(loggingLevel: .error, enableWebSocketInvites: true, rtcStatsTelemetry: false) + self.client = VGVonageClient(config) + client.delegate = self + } + + private func refreshToken() async { + if let credentials = getCredentials() { + let body = Auth.Body(name: credentials.0, password: credentials.1, displayName: nil) + do { + let authResponse: Auth.RefreshResponse = try await RemoteLoader.post(path: Auth.refreshPath, body: body) + self.token = authResponse.token + try await self.client.refreshSession(authResponse.token) + } catch { + print(error) + } + } + } +} + +// MARK: - VGClientDelegate + +extension ClientManager: VGClientDelegate { + func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) { + incomingCallSubject.send(.invite(callId: callId, caller: caller)) + } + + func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: VGCallId, with reason: VGVoiceInviteCancelReason) { + incomingCallSubject.send(.inviteCancel(callId: callId, reason: reason)) + } + + func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) { + callEventSubject.send(.hangup(callId: callId, reason: reason)) + } + + func voiceClient(_ client: VGVoiceClient, didReceiveLegStatusUpdateForCall callId: VGCallId, withLegId legId: String, andStatus status: VGLegStatus) { + callEventSubject.send(.update(callId: callId, legId: legId, status: status)) + } + + func chatClient(_ client: VGChatClient, didReceiveConversationEvent event: VGConversationEvent) { + messageSubject.send(event) + } + + func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) { + Task { + await refreshToken() + } + } +} + +// MARK: - Keychain Storage + +extension ClientManager { + private func storeCredentials(username: String, password: String) { + if let passwordData = password.data(using: .utf8) { + let keychainItem = [ + kSecClass: kSecClassInternetPassword, + kSecAttrServer: Constants.keychainServer, + kSecReturnData: true, + kSecReturnAttributes: true, + kSecAttrAccount: username, + kSecValueData: passwordData + ] as CFDictionary + + let status = SecItemAdd(keychainItem, nil) + print("Keychain storing finished with status: \(status)") + } + } + + func getCredentials() -> (String, String)? { + let query = [ + kSecClass: kSecClassInternetPassword, + kSecAttrServer: Constants.keychainServer, + kSecReturnAttributes: true, + kSecReturnData: true, + kSecMatchLimit: 1 + ] as CFDictionary + + var result: AnyObject? + let status = SecItemCopyMatching(query, &result) + print("Keychain querying finished with status: \(status)") + + if let resultArray = result as? NSDictionary, + let username = resultArray[kSecAttrAccount] as? String, + let passwordData = resultArray[kSecValueData] as? Data, + let password = String(data: passwordData, encoding: .utf8) { + return (username, password) + } else { + return nil + } + } + + private func deleteCredentials(username: String) { + let query = [ + kSecClass: kSecClassInternetPassword, + kSecAttrServer: Constants.keychainServer, + kSecAttrAccount: username + ] as CFDictionary + + SecItemDelete(query) + } +} diff --git a/client-ios/Vapp/Helpers/Constants.swift b/client-ios/Vapp/Helpers/Constants.swift new file mode 100644 index 0000000..33af6ee --- /dev/null +++ b/client-ios/Vapp/Helpers/Constants.swift @@ -0,0 +1,10 @@ +// +// Constants.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 13/03/2024. +// + +struct Constants { + static let keychainServer = "developer.vonage.com" +} diff --git a/client-ios/Vapp/Helpers/Models.swift b/client-ios/Vapp/Helpers/Models.swift new file mode 100644 index 0000000..131cb26 --- /dev/null +++ b/client-ios/Vapp/Helpers/Models.swift @@ -0,0 +1,84 @@ +// +// Models.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import Foundation +import ExyteChat +import VonageClientSDK + +extension VGConversation: Identifiable {} + +struct Users: Codable, Hashable { + static let path = "/users" + + struct List: Codable { + typealias Response = [User] + } + + struct User: Codable, Hashable, Identifiable { + let id: String + let name: String + let displayName: String + let imageURL: String? + + enum CodingKeys: String, CodingKey { + case id, name + case imageURL = "image_url" + case displayName = "display_name" + } + } +} + +struct Auth: Codable { + static let signupPath = "/signup" + static let loginPath = "/login" + static let refreshPath = "/token" + + struct Body: Codable { + let name: String + let password: String + let displayName: String? + + enum CodingKeys: String, CodingKey { + case name, password + case displayName = "display_name" + } + } + + struct Response: Codable { + let user: Users.User + let token: String + let users: [Users.User] + } + + struct RefreshResponse: Codable { + let token: String + } +} + +struct APIError: Codable { + let type: String? + let title: String? + let detail: String? + let invalidParameters: [[String: String]]? + + enum CodingKeys: String, CodingKey { + case type, title, detail + case invalidParameters = "invalid_parameters" + } + + var description: String { + var descriptionString: String = self.detail ?? "" + + if let invalidParameters = invalidParameters { + for invalidParameter in invalidParameters { + descriptionString += "\n \(invalidParameter.description)" + } + } + + return descriptionString + } +} diff --git a/client-ios/Vapp/Helpers/RemoteLoader.swift b/client-ios/Vapp/Helpers/RemoteLoader.swift new file mode 100644 index 0000000..97f7ade --- /dev/null +++ b/client-ios/Vapp/Helpers/RemoteLoader.swift @@ -0,0 +1,53 @@ +// +// RemoteLoader.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import Foundation + +enum RemoteLoaderError: Error { + case url + case api(error: APIError) + case misc(error: Error) +} + +final class RemoteLoader { + + static let baseURL = "https://neru-febe6726-vapp-dev.euw1.runtime.vonage.cloud" + + static func post(path: String, + authToken: String? = nil, + body: T?) async throws -> U { + guard let url = URL(string: baseURL + path) else { + throw RemoteLoaderError.url + } + + var request = URLRequest(url: url) + + if let body = body, let encodedBody = try? JSONEncoder().encode(body) { + request.httpMethod = "POST" + request.httpBody = encodedBody + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + + if let token = authToken { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + do { + let (data, _) = try await URLSession.shared.data(for: request) + if let response = try? JSONDecoder().decode(U.self, from: data) { + return response + } else if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { + throw RemoteLoaderError.api(error: apiError) + } + } catch { + throw RemoteLoaderError.misc(error: error) + } + + fatalError("Should not be hit") + } +} + diff --git a/client-ios/Vapp/Home/HomeView.swift b/client-ios/Vapp/Home/HomeView.swift new file mode 100644 index 0000000..6b60eec --- /dev/null +++ b/client-ios/Vapp/Home/HomeView.swift @@ -0,0 +1,122 @@ +// +// HomeView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import SwiftUI +import VonageClientSDK + +struct HomeView: View { + @StateObject var viewModel = HomeViewModel() + @State private var selectedConversation: VGConversation? + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + if viewModel.isLoading { + ProgressView() + } else { + if viewModel.conversations.isEmpty { + Button("Start a Conversation") { + viewModel.showNewConversation = true + }.buttonStyle(.bordered) + } else { + List(viewModel.conversations) { conversation in + HStack { + Text(conversation.displayName ?? "") + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + selectedConversation = conversation + } + .swipeActions { + Button("Delete") { + Task { + await viewModel.deleteConversation(conversation.id) + } + } + .tint(.red) + } + } + .refreshable { + await triggerConversationLoad() + } + } + } + } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) + } + .navigationTitle("V App") + .navigationBarTitleDisplayMode(.large) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button(action: { + viewModel.showNewConversation = true + }) { + Image(systemName: "plus") + } + + Button(action: { + viewModel.showUsers = true + }) { + Image(systemName: "person.circle") + } + + Button(action: { + viewModel.showSettings = true + }) { + Image(systemName: "gear") + } + } + } + .navigationDestination(item: $selectedConversation) { conversation in + ChatView(viewModel: .init(conversationId: conversation.id, conversationDisplayName: conversation.displayName)) + } + .navigationDestination(isPresented: $viewModel.callAccepted) { + CallView(viewModel: .init(callId: viewModel.incomingCallId, callee: viewModel.incomingCaller)) + } + .sheet(isPresented: $viewModel.showUsers) { + UsersView() + } + .sheet(isPresented: $viewModel.showSettings, onDismiss: { + if viewModel.shouldLogout { + dismiss() + } + }) { + SettingsView(viewModel: viewModel) + } + .sheet(isPresented: $viewModel.showIncomingCall) { + IncomingCallView(viewModel: viewModel) + .presentationDetents([.fraction(0.3)]) + .presentationDragIndicator(.hidden) + } + .sheet(isPresented: $viewModel.showIncomingChat) { + IncomingChatView(viewModel: viewModel) + .presentationDetents([.fraction(0.3)]) + .presentationDragIndicator(.hidden) + } + .sheet(isPresented: $viewModel.showNewConversation, onDismiss: { + Task { + await triggerConversationLoad() + } + }, content: { + CreateConversationView() + }) + .task { + await triggerConversationLoad() + } + } + + func triggerConversationLoad() async { + viewModel.isLoading = true + await viewModel.loadConversations() + viewModel.isLoading = false + } +} diff --git a/client-ios/Vapp/Home/HomeViewModel.swift b/client-ios/Vapp/Home/HomeViewModel.swift new file mode 100644 index 0000000..228953d --- /dev/null +++ b/client-ios/Vapp/Home/HomeViewModel.swift @@ -0,0 +1,140 @@ +// +// HomeViewModel.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 28/03/2024. +// + +import Foundation +import Combine +import VonageClientSDK + +final class HomeViewModel: ObservableObject { + private let clientManager = ClientManager.shared + private var subscriptions = Set() + + @Published var isLoading = false + @Published var conversations: [VGConversation] = [] + + @Published var showNewConversation = false + @Published var showIncomingCall = false + @Published var showIncomingChat = false + @Published var showSettings = false + @Published var showUsers = false + + @Published var shouldLogout = false + + @Published var callAccepted = false + @Published var chatInviteAccepted = false + + @Published var errorContainer = (hasError: false, text: "") + + var incomingCallId: String! + var incomingCaller: String! + + var incomingConversationId: String! + var incomingInviter: String! + + init() { + clientManager.onCall + .receive(on: DispatchQueue.main) + .sink { [weak self] call in + switch call { + case .invite(let callId, let caller): + self?.showIncomingCall = true + self?.incomingCallId = callId + self?.incomingCaller = caller + case .inviteCancel: + self?.showIncomingCall = false + } + } + .store(in: &subscriptions) + + clientManager.onEvent + .receive(on: DispatchQueue.main) + .filter { $0.kind == .memberInvited } + .filter { !($0.from is VGSystem) } + .map { $0 as! VGMemberInvitedEvent } + .sink { [weak self] event in + guard event.body.user.name == self?.clientManager.username else { return } + self?.incomingConversationId = event.conversationId + self?.incomingInviter = "Unknown" + + if let userInfo = event.from as? VGEmbeddedInfo { + self?.incomingInviter = userInfo.user.name + } + + self?.showIncomingChat = true + }.store(in: &subscriptions) + } + + @MainActor + func loadConversations() async { + do { + let conversationPage = try await clientManager.client.getConversations() + conversations = conversationPage.conversations.filter { !$0.name.contains("NAM") } + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func deleteConversation(_ id: String) async { + do { + try await clientManager.client.deleteConversation(id) + conversations.removeAll { $0.id == id } + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func acceptCallInvite() async { + do { + showIncomingCall = false + try await clientManager.client.answer(incomingCallId) + callAccepted = true + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func rejectCallInvite() async { + do { + try await clientManager.client.reject(incomingCallId) + showIncomingCall = false + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func acceptChatInvite() async { + do { + showIncomingChat = false + _ = try await clientManager.client.joinConversation(incomingConversationId) + await loadConversations() + chatInviteAccepted = true + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + func rejectChatInvite() async { + await MainActor.run { + showIncomingChat = false + } + } + + @MainActor + func logout() async { + do { + showSettings = false + try await clientManager.logout() + shouldLogout = true + } catch { + errorContainer = (true, error.localizedDescription) + } + } +} diff --git a/client-ios/Vapp/IncomingCallView.swift b/client-ios/Vapp/IncomingCallView.swift new file mode 100644 index 0000000..f56b85c --- /dev/null +++ b/client-ios/Vapp/IncomingCallView.swift @@ -0,0 +1,36 @@ +// +// IncomingCallView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 22/03/2024. +// + +import SwiftUI + +struct IncomingCallView: View { + @ObservedObject var viewModel: HomeViewModel + + var body: some View { + VStack { + Text("Call from \(viewModel.incomingCaller)") + .padding(16) + HStack { + Button("Accept") { + Task { + await viewModel.acceptCallInvite() + } + } + .tint(.green) + .buttonStyle(.bordered) + + Button("Reject") { + Task { + await viewModel.rejectCallInvite() + } + } + .tint(.red) + .buttonStyle(.bordered) + } + } + } +} diff --git a/client-ios/Vapp/IncomingChatView.swift b/client-ios/Vapp/IncomingChatView.swift new file mode 100644 index 0000000..ccae1c5 --- /dev/null +++ b/client-ios/Vapp/IncomingChatView.swift @@ -0,0 +1,36 @@ +// +// IncomingChatView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 25/03/2024. +// + +import SwiftUI + +struct IncomingChatView: View { + @ObservedObject var viewModel: HomeViewModel + + var body: some View { + VStack { + Text("Chat Invite from \(viewModel.incomingInviter)") + .padding(16) + HStack { + Button("Accept") { + Task { + await viewModel.acceptChatInvite() + } + } + .tint(.green) + .buttonStyle(.bordered) + + Button("Reject") { + Task { + await viewModel.rejectChatInvite() + } + } + .tint(.red) + .buttonStyle(.bordered) + } + } + } +} diff --git a/client-ios/Vapp/Preview Content/Preview Assets.xcassets/Contents.json b/client-ios/Vapp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/client-ios/Vapp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/client-ios/Vapp/SettingsView.swift b/client-ios/Vapp/SettingsView.swift new file mode 100644 index 0000000..c420919 --- /dev/null +++ b/client-ios/Vapp/SettingsView.swift @@ -0,0 +1,30 @@ +// +// SettingsView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 27/03/2024. +// + +import SwiftUI + +struct SettingsView: View { + @ObservedObject var viewModel: HomeViewModel + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + VStack { + Button("Logout") { + Task { + await viewModel.logout() + dismiss() + } + } + .tint(.red) + .buttonStyle(.bordered) + } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/client-ios/Vapp/UsersView.swift b/client-ios/Vapp/UsersView.swift new file mode 100644 index 0000000..f54b146 --- /dev/null +++ b/client-ios/Vapp/UsersView.swift @@ -0,0 +1,69 @@ +// +// UsersView.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 19/03/2024. +// + +import SwiftUI +import VonageClientSDK + +struct UsersView: View { + @StateObject var viewModel = UsersViewModel() + + var body: some View { + NavigationStack { + VStack { + List(viewModel.users) { user in + HStack { + Text(user.name) + Spacer() + Button(action: { + Task { + await viewModel.startCall(callee: user.name) + } + }) { + Image(systemName: "phone.circle.fill") + }.buttonStyle(.borderless) + } + } + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error Making Call"), message: Text(viewModel.errorContainer.text)) + } + .navigationTitle("Users") + .navigationBarTitleDisplayMode(.inline) + .navigationDestination(isPresented: $viewModel.callCreated) { + CallView(viewModel: .init(callId: viewModel.callId, callee: viewModel.callee)) + } + } + } +} + +final class UsersViewModel: ObservableObject { + private let clientManager = ClientManager.shared + + @Published var isLoading = false + @Published var callCreated = false + @Published var errorContainer = (hasError: false, text: "") + + var callId = "" + var callee = "" + + var users: [Users.User] { + clientManager.users + } + + @MainActor + func startCall(callee: String) async { + do { + self.callId = try await clientManager.client.serverCall(["to" : callee]) + self.callee = callee + callCreated = true + + } catch { + errorContainer = (true, error.localizedDescription) + } + } + +} diff --git a/client-ios/Vapp/Vapp.swift b/client-ios/Vapp/Vapp.swift new file mode 100644 index 0000000..c2c44f6 --- /dev/null +++ b/client-ios/Vapp/Vapp.swift @@ -0,0 +1,17 @@ +// +// Vapp.swift +// Vapp +// +// Created by Abdulhakim Ajetunmobi on 12/03/2024. +// + +import SwiftUI + +@main +struct Vapp: App { + var body: some Scene { + WindowGroup { + LogInView() + } + } +} From 734f357d6f8dfbfc047b95612d4248ff29bd5976 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Mon, 29 Apr 2024 17:56:36 +0100 Subject: [PATCH 2/7] add profile image --- client-ios/Vapp/Auth/SignUpView.swift | 1 + client-ios/Vapp/ChatInviteView.swift | 2 +- client-ios/Vapp/Helpers/ClientManager.swift | 14 ++- client-ios/Vapp/Helpers/Models.swift | 14 ++- client-ios/Vapp/Helpers/RemoteLoader.swift | 47 ++++++++- client-ios/Vapp/Home/HomeView.swift | 2 +- client-ios/Vapp/Home/HomeViewModel.swift | 14 ++- client-ios/Vapp/SettingsView.swift | 109 ++++++++++++++++++-- client-ios/Vapp/UsersView.swift | 1 - 9 files changed, 185 insertions(+), 19 deletions(-) diff --git a/client-ios/Vapp/Auth/SignUpView.swift b/client-ios/Vapp/Auth/SignUpView.swift index 89157a1..e355f48 100644 --- a/client-ios/Vapp/Auth/SignUpView.swift +++ b/client-ios/Vapp/Auth/SignUpView.swift @@ -50,6 +50,7 @@ final class SignUpViewModel: ObservableObject { @Published var isLoading = false @Published var errorContainer = (hasError: false, text: "") + @MainActor func signUp() async { do { try await ClientManager.shared.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath) diff --git a/client-ios/Vapp/ChatInviteView.swift b/client-ios/Vapp/ChatInviteView.swift index dfb59d3..d293911 100644 --- a/client-ios/Vapp/ChatInviteView.swift +++ b/client-ios/Vapp/ChatInviteView.swift @@ -69,7 +69,7 @@ final class ChatInviteViewModel: ObservableObject { await withThrowingTaskGroup(of: Void.self) { group in for username in usernames { group.addTask { - let memberId = try await self.clientManager.client.inviteToConversation(self.conversationId, username: username) + _ = try await self.clientManager.client.inviteToConversation(self.conversationId, username: username) } } } diff --git a/client-ios/Vapp/Helpers/ClientManager.swift b/client-ios/Vapp/Helpers/ClientManager.swift index b1accc0..efbcb6c 100644 --- a/client-ios/Vapp/Helpers/ClientManager.swift +++ b/client-ios/Vapp/Helpers/ClientManager.swift @@ -12,8 +12,8 @@ import VonageClientSDK final class ClientManager: NSObject, ObservableObject { static let shared = ClientManager() + public var user: Users.User? = nil public var token: String? = nil - public var username = "" public var client: VGVonageClient! @Published var isAuthed = false @@ -60,7 +60,7 @@ final class ClientManager: NSObject, ObservableObject { let authResponse: Auth.Response = try await RemoteLoader.post(path: path, body: body) self.token = authResponse.token - self.username = username + self.user = authResponse.user try await client?.createSession(authResponse.token) if shouldStoreCredentials { @@ -80,15 +80,19 @@ final class ClientManager: NSObject, ObservableObject { } public func logout() async throws { - try await client.deleteSession() - deleteCredentials(username: username) + if let username = user?.name { + try await client.deleteSession() + deleteCredentials(username: username) + } else { + // TODO: throw error + } } // MARK: - Private private func initializeClient() { VGVonageClient.isUsingCallKit = false - let config = VGClientInitConfig(loggingLevel: .error, enableWebSocketInvites: true, rtcStatsTelemetry: false) + let config = VGClientInitConfig(loggingLevel: .error, region: .EU, enableWebSocketInvites: true, rtcStatsTelemetry: false) self.client = VGVonageClient(config) client.delegate = self } diff --git a/client-ios/Vapp/Helpers/Models.swift b/client-ios/Vapp/Helpers/Models.swift index 131cb26..5dc1486 100644 --- a/client-ios/Vapp/Helpers/Models.swift +++ b/client-ios/Vapp/Helpers/Models.swift @@ -6,7 +6,6 @@ // import Foundation -import ExyteChat import VonageClientSDK extension VGConversation: Identifiable {} @@ -59,6 +58,19 @@ struct Auth: Codable { } } +struct ImageUpload: Codable { + static let profilePath = "/users/image" + static let chatPath = "/image" + + struct Response: Codable { + let imageURL: String + + enum CodingKeys: String, CodingKey { + case imageURL = "image_url" + } + } +} + struct APIError: Codable { let type: String? let title: String? diff --git a/client-ios/Vapp/Helpers/RemoteLoader.swift b/client-ios/Vapp/Helpers/RemoteLoader.swift index 97f7ade..fc4fef3 100644 --- a/client-ios/Vapp/Helpers/RemoteLoader.swift +++ b/client-ios/Vapp/Helpers/RemoteLoader.swift @@ -9,6 +9,7 @@ import Foundation enum RemoteLoaderError: Error { case url + case unknown case api(error: APIError) case misc(error: Error) } @@ -16,7 +17,7 @@ enum RemoteLoaderError: Error { final class RemoteLoader { static let baseURL = "https://neru-febe6726-vapp-dev.euw1.runtime.vonage.cloud" - + static func post(path: String, authToken: String? = nil, body: T?) async throws -> U { @@ -42,12 +43,54 @@ final class RemoteLoader { return response } else if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { throw RemoteLoaderError.api(error: apiError) + } else { + throw RemoteLoaderError.unknown } } catch { throw RemoteLoaderError.misc(error: error) } + } + + static func multipart(path: String, + mimeType: String, + authToken: String, + data: Data) async throws -> U { + + guard let url = URL(string: baseURL + path) else { + throw RemoteLoaderError.url + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + + let boundary = "Boundary-\(UUID().uuidString)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") + + var body = Data() + + body.append("--\(boundary)\r\n".data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"image\"; filename=\"image\"\r\n".data(using: .utf8)!) + body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) + body.append(data) + body.append("\r\n".data(using: .utf8)!) - fatalError("Should not be hit") + body.append("--\(boundary)--\r\n".data(using: .utf8)!) + + request.httpBody = body + + do { + let (data, _) = try await URLSession.shared.data(for: request) + if let response = try? JSONDecoder().decode(U.self, from: data) { + return response + } else if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { + throw RemoteLoaderError.api(error: apiError) + } else { + throw RemoteLoaderError.unknown + } + } catch { + throw RemoteLoaderError.misc(error: error) + } } } diff --git a/client-ios/Vapp/Home/HomeView.swift b/client-ios/Vapp/Home/HomeView.swift index 6b60eec..474e8a5 100644 --- a/client-ios/Vapp/Home/HomeView.swift +++ b/client-ios/Vapp/Home/HomeView.swift @@ -90,7 +90,7 @@ struct HomeView: View { dismiss() } }) { - SettingsView(viewModel: viewModel) + SettingsView(homeViewModel: viewModel) } .sheet(isPresented: $viewModel.showIncomingCall) { IncomingCallView(viewModel: viewModel) diff --git a/client-ios/Vapp/Home/HomeViewModel.swift b/client-ios/Vapp/Home/HomeViewModel.swift index 228953d..8ee05e1 100644 --- a/client-ios/Vapp/Home/HomeViewModel.swift +++ b/client-ios/Vapp/Home/HomeViewModel.swift @@ -35,7 +35,13 @@ final class HomeViewModel: ObservableObject { var incomingConversationId: String! var incomingInviter: String! + var user: Users.User? + var token: String? + init() { + self.user = clientManager.user + self.token = clientManager.token + clientManager.onCall .receive(on: DispatchQueue.main) .sink { [weak self] call in @@ -56,7 +62,7 @@ final class HomeViewModel: ObservableObject { .filter { !($0.from is VGSystem) } .map { $0 as! VGMemberInvitedEvent } .sink { [weak self] event in - guard event.body.user.name == self?.clientManager.username else { return } + guard event.body.user.name == self?.clientManager.user?.name else { return } self?.incomingConversationId = event.conversationId self?.incomingInviter = "Unknown" @@ -137,4 +143,10 @@ final class HomeViewModel: ObservableObject { errorContainer = (true, error.localizedDescription) } } + + func updateUser(imageURL: String?) { + guard let user = user, let imageURL = imageURL else { return } + let newUser = Users.User(id: user.id, name: user.name, displayName: user.displayName, imageURL: imageURL) + self.user = newUser + } } diff --git a/client-ios/Vapp/SettingsView.swift b/client-ios/Vapp/SettingsView.swift index c420919..9f994e5 100644 --- a/client-ios/Vapp/SettingsView.swift +++ b/client-ios/Vapp/SettingsView.swift @@ -6,25 +6,120 @@ // import SwiftUI +import PhotosUI struct SettingsView: View { - @ObservedObject var viewModel: HomeViewModel + @ObservedObject var homeViewModel: HomeViewModel + @ObservedObject var viewModel = SettingsViewModel() @Environment(\.dismiss) var dismiss + @State var selectedPhoto: PhotosPickerItem? + var body: some View { NavigationStack { VStack { - Button("Logout") { - Task { - await viewModel.logout() - dismiss() + if viewModel.isLoading { + ProgressView() + } else { + PhotosPicker(selection: $selectedPhoto) { + ZStack { + if let url = viewModel.imageURL { + AsyncImage(url: url) { phase in + switch phase { + case .empty: + ProgressView() + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(width: 200, height: 200) + .clipShape(Circle()) + case .failure: + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .clipShape(Circle()) + @unknown default: + EmptyView() + } + } + } else { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .clipShape(Circle()) + } + } + }.padding(16) + + VStack { + Text("Username: \(homeViewModel.user?.name ?? "")") + Text("Display Name: \(homeViewModel.user?.displayName ?? "")") + }.padding(16) + + Button("Logout") { + Task { + await homeViewModel.logout() + dismiss() + } } + .tint(.red) + .buttonStyle(.bordered) + } + } + .onAppear { + viewModel.updateURL(urlString: homeViewModel.user?.imageURL) + } + .task(id: selectedPhoto) { + if let selectedPhoto { + viewModel.isLoading = true + await viewModel.updateUserImage(selectedPhoto, authToken: homeViewModel.token) + homeViewModel.updateUser(imageURL: viewModel.imageURL?.absoluteString) + viewModel.isLoading = false } - .tint(.red) - .buttonStyle(.bordered) + } + .alert(isPresented: $viewModel.errorContainer.hasError) { + Alert(title: Text("Error Making Call"), message: Text(viewModel.errorContainer.text)) } .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) } } } + +final class SettingsViewModel: ObservableObject { + @Published var imageURL: URL? + @Published var isLoading = false + @Published var errorContainer = (hasError: false, text: "") + + @MainActor + func updateURL(urlString: String?) { + if let urlString { + imageURL = URL(string: urlString) + } + } + + @MainActor + func updateUserImage(_ selectedItem: PhotosPickerItem, authToken: String?) async { + do { + guard let authToken else { + errorContainer = (true, "Auth Token Missing") + return + } + + guard let imageData = try await selectedItem.loadTransferable(type: Data.self), + let mimeType = selectedItem.supportedContentTypes.first?.preferredMIMEType else { + errorContainer = (true, "Error Loading Image") + return + } + + let uploadResponse: ImageUpload.Response = try await RemoteLoader.multipart(path: ImageUpload.profilePath, mimeType: mimeType, authToken: authToken, data: imageData) + updateURL(urlString: uploadResponse.imageURL) + + } catch { + errorContainer = (true, error.localizedDescription) + } + } +} diff --git a/client-ios/Vapp/UsersView.swift b/client-ios/Vapp/UsersView.swift index f54b146..80fab1c 100644 --- a/client-ios/Vapp/UsersView.swift +++ b/client-ios/Vapp/UsersView.swift @@ -60,7 +60,6 @@ final class UsersViewModel: ObservableObject { self.callId = try await clientManager.client.serverCall(["to" : callee]) self.callee = callee callCreated = true - } catch { errorContainer = (true, error.localizedDescription) } From afb01ef37b66b117952ce17e2149a4b8be949b09 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Tue, 30 Apr 2024 12:40:39 +0100 Subject: [PATCH 3/7] Add image sending --- client-ios/Vapp/Auth/LogInView.swift | 17 ++- client-ios/Vapp/Auth/SignUpView.swift | 11 +- client-ios/Vapp/CallView.swift | 1 - client-ios/Vapp/ChatView.swift | 119 +++++++++++++++----- client-ios/Vapp/Helpers/ClientManager.swift | 26 ++--- client-ios/Vapp/SettingsView.swift | 6 +- client-ios/Vapp/UsersView.swift | 22 ++++ 7 files changed, 147 insertions(+), 55 deletions(-) diff --git a/client-ios/Vapp/Auth/LogInView.swift b/client-ios/Vapp/Auth/LogInView.swift index ee5599e..303459a 100644 --- a/client-ios/Vapp/Auth/LogInView.swift +++ b/client-ios/Vapp/Auth/LogInView.swift @@ -9,7 +9,6 @@ import SwiftUI struct LogInView: View { @StateObject var viewModel = LogInViewModel() - @StateObject var clientManager = ClientManager.shared var body: some View { NavigationStack { @@ -43,7 +42,7 @@ struct LogInView: View { .alert(isPresented: $viewModel.errorContainer.hasError) { Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) } - .navigationDestination(isPresented: $clientManager.isAuthed) { + .navigationDestination(isPresented: $viewModel.showHomeView) { HomeView() } .navigationDestination(isPresented: $viewModel.showSignUp) { @@ -59,20 +58,30 @@ final class LogInViewModel: ObservableObject { @Published var isLoading = false @Published var showSignUp = false + @Published var showHomeView = false @Published var errorContainer = (hasError: false, text: "") + private let clientManager = ClientManager.shared + @MainActor func logIn() async { do { - try await ClientManager.shared.auth(username: username, password: password, path: Auth.loginPath) + try await clientManager.auth(username: username, password: password, path: Auth.loginPath) username = "" password = "" + if clientManager.isAuthed { + showHomeView = true + } } catch { errorContainer = (true, error.localizedDescription) } } + @MainActor func attemptStoredLogIn() async { - await ClientManager.shared.attemptStoredLogIn() + await clientManager.attemptStoredLogIn() + if clientManager.isAuthed { + showHomeView = true + } } } diff --git a/client-ios/Vapp/Auth/SignUpView.swift b/client-ios/Vapp/Auth/SignUpView.swift index e355f48..d882145 100644 --- a/client-ios/Vapp/Auth/SignUpView.swift +++ b/client-ios/Vapp/Auth/SignUpView.swift @@ -9,7 +9,6 @@ import SwiftUI struct SignUpView: View { @StateObject var viewModel = SignUpViewModel() - @StateObject var clientManager = ClientManager.shared var body: some View { NavigationStack { @@ -35,7 +34,7 @@ struct SignUpView: View { .alert(isPresented: $viewModel.errorContainer.hasError) { Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) } - .navigationDestination(isPresented: $clientManager.isAuthed) { + .navigationDestination(isPresented: $viewModel.showHomeView) { HomeView() } } @@ -48,15 +47,21 @@ final class SignUpViewModel: ObservableObject { @Published var password = "" @Published var isLoading = false + @Published var showHomeView = false @Published var errorContainer = (hasError: false, text: "") + private let clientManager = ClientManager.shared + @MainActor func signUp() async { do { - try await ClientManager.shared.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath) + try await clientManager.auth(username: username, password: password, displayName: displayName, path: Auth.signupPath) username = "" password = "" displayName = "" + if clientManager.isAuthed { + showHomeView = true + } } catch { errorContainer = (true, error.localizedDescription) } diff --git a/client-ios/Vapp/CallView.swift b/client-ios/Vapp/CallView.swift index b454d99..4472c55 100644 --- a/client-ios/Vapp/CallView.swift +++ b/client-ios/Vapp/CallView.swift @@ -13,7 +13,6 @@ struct CallView: View { @StateObject var viewModel: CallViewModel var body: some View { - NavigationStack { VStack { Text(viewModel.callStatus) diff --git a/client-ios/Vapp/ChatView.swift b/client-ios/Vapp/ChatView.swift index f2ef410..b538d06 100644 --- a/client-ios/Vapp/ChatView.swift +++ b/client-ios/Vapp/ChatView.swift @@ -7,12 +7,14 @@ import Combine import SwiftUI +import PhotosUI import VonageClientSDK struct ChatView: View { @StateObject var viewModel: ChatViewModel @State private var message: String = "" @State private var listTopId: Int? + @State private var selectedPhoto: PhotosPickerItem? var body: some View { NavigationStack { @@ -29,10 +31,23 @@ struct ChatView: View { let displayText = viewModel.generateDisplayText(event) Text(displayText.body) .frame(maxWidth: .infinity, alignment: .center) - case.messageText: + case .messageText: let displayText = viewModel.generateDisplayText(event) Text(displayText.body) .frame(maxWidth: .infinity, alignment: displayText.isUser ? .trailing : .leading) + case .messageImage: + let urlString = viewModel.generateDisplayText(event) + AsyncImage(url: URL(string: urlString.body)) { phase in + if let image = phase.image { + image + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + } else { + ProgressView() + .frame(width: 200, height: 200) + } + }.frame(maxWidth: .infinity, alignment: urlString.isUser ? .trailing : .leading) default: EmptyView() } @@ -55,24 +70,40 @@ struct ChatView: View { HStack { TextField("Message", text: $message) - Button("Send") { - Task { - await viewModel.sendMessage(message) - self.message = "" - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - withAnimation(.easeInOut(duration: 1)) { - proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom) + if message.isEmpty { + Button { + viewModel.showPhotoPicker = true + } label: { + Image(systemName: "photo.fill") + }.buttonStyle(.bordered) + } else { + Button { + Task { + await viewModel.sendMessage(message) + self.message = "" + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + withAnimation(.easeInOut(duration: 1)) { + proxy.scrollTo(viewModel.events.first!.id, anchor: .bottom) + } } } - } - }.buttonStyle(.bordered) + } label: { + Image(systemName: "paperplane") + }.buttonStyle(.bordered) + } }.padding(8) } } } } } + .photosPicker(isPresented: $viewModel.showPhotoPicker, selection: $selectedPhoto) + .task(id: selectedPhoto) { + if let selectedPhoto { + await viewModel.sendImage(selectedPhoto) + } + } .alert(isPresented: $viewModel.errorContainer.hasError) { Alert(title: Text("Error"), message: Text(viewModel.errorContainer.text)) } @@ -100,7 +131,6 @@ struct ChatView: View { } final class ChatViewModel: ObservableObject { - private var memberId: String? private var cursor: String? = nil private let clientManager = ClientManager.shared @@ -108,6 +138,7 @@ final class ChatViewModel: ObservableObject { @Published var events: [VGPersistentConversationEvent] = [] @Published var showInviteUser = false + @Published var showPhotoPicker = false @Published var isLoading = false @Published var errorContainer = (hasError: false, text: "") @@ -143,22 +174,8 @@ final class ChatViewModel: ObservableObject { } } - private func getEvents() async -> [VGPersistentConversationEvent] { - do { - let params = VGGetConversationEventsParameters(order: .desc, pageSize: cursorSize, cursor: cursor) - let eventsPage = try await clientManager.client.getConversationEvents(conversationId, parameters: params) - cursor = eventsPage.nextCursor - return eventsPage.events - } catch { - await MainActor.run { - errorContainer = (true, error.localizedDescription) - } - } - - return [] - } - func loadEarlierEvents() async { + guard cursor != nil else { return } let earlierEvents = await getEvents() await MainActor.run { @@ -178,6 +195,28 @@ final class ChatViewModel: ObservableObject { } } + func sendImage(_ selectedItem: PhotosPickerItem) async { + do { + guard let authToken = clientManager.token else { + errorContainer = (true, "Auth Token Missing") + return + } + + guard let imageData = try await selectedItem.loadTransferable(type: Data.self), + let mimeType = selectedItem.supportedContentTypes.first?.preferredMIMEType else { + errorContainer = (true, "Error Loading Image") + return + } + + let uploadResponse: ImageUpload.Response = try await RemoteLoader.multipart(path: ImageUpload.chatPath, mimeType: mimeType, authToken: authToken, data: imageData) + + _ = try await clientManager.client.sendMessageImageEvent(conversationId, imageUrl: URL(string: uploadResponse.imageURL)!) + + } catch { + errorContainer = (true, error.localizedDescription) + } + } + func generateDisplayText(_ event: VGPersistentConversationEvent) -> (body: String, isUser: Bool) { var from = "System" @@ -204,6 +243,15 @@ final class ChatViewModel: ObservableObject { } return ("\(from) \(messageTextEvent.body.text)", isUser) + case .messageImage: + let messageImageEvent = event as! VGMessageImageEvent + var isUser = false + if let userInfo = messageImageEvent.from as? VGEmbeddedInfo { + isUser = userInfo.memberId == memberId + from = isUser ? "" : "\(userInfo.user.name): " + } + + return (messageImageEvent.body.imageUrl, isUser) default: return ("", false) } @@ -211,10 +259,25 @@ final class ChatViewModel: ObservableObject { // MARK: - Private + private func getEvents() async -> [VGPersistentConversationEvent] { + do { + let params = VGGetConversationEventsParameters(order: .desc, pageSize: cursorSize, cursor: cursor) + let eventsPage = try await clientManager.client.getConversationEvents(conversationId, parameters: params) + cursor = eventsPage.nextCursor + return eventsPage.events + } catch { + await MainActor.run { + errorContainer = (true, error.localizedDescription) + } + } + + return [] + } + private func getMemberId() async { do { let member = try await clientManager.client.getConversationMember(conversationId, memberId: "me") - + if member.state == .joined { memberId = member.id return diff --git a/client-ios/Vapp/Helpers/ClientManager.swift b/client-ios/Vapp/Helpers/ClientManager.swift index efbcb6c..455a6c0 100644 --- a/client-ios/Vapp/Helpers/ClientManager.swift +++ b/client-ios/Vapp/Helpers/ClientManager.swift @@ -20,7 +20,7 @@ final class ClientManager: NSObject, ObservableObject { var users: [Users.User] = [] // Chat Publisher - private var handledEventKinds: Set = [.memberInvited, .memberJoined, .memberLeft, .messageText] + private var handledEventKinds: Set = [.memberInvited, .memberJoined, .memberLeft, .messageText, .messageImage] private let messageSubject = PassthroughSubject() public var onEvent: AnyPublisher { messageSubject @@ -55,7 +55,7 @@ final class ClientManager: NSObject, ObservableObject { // MARK: - Public - public func auth(username: String, password: String, displayName: String? = nil, path: String, shouldStoreCredentials: Bool = true) async throws { + public func auth(username: String, password: String, displayName: String? = nil, path: String) async throws { let body = Auth.Body(name: username, password: password, displayName: displayName) let authResponse: Auth.Response = try await RemoteLoader.post(path: path, body: body) @@ -63,9 +63,7 @@ final class ClientManager: NSObject, ObservableObject { self.user = authResponse.user try await client?.createSession(authResponse.token) - if shouldStoreCredentials { - storeCredentials(username: username, password: password) - } + storeCredentials(username: username, password: password) await MainActor.run { isAuthed = true @@ -75,17 +73,13 @@ final class ClientManager: NSObject, ObservableObject { public func attemptStoredLogIn() async { if let credentials = getCredentials() { - try? await auth(username: credentials.0, password: credentials.1, path: Auth.loginPath, shouldStoreCredentials: false) + try? await auth(username: credentials.0, password: credentials.1, path: Auth.loginPath) } } public func logout() async throws { - if let username = user?.name { - try await client.deleteSession() - deleteCredentials(username: username) - } else { - // TODO: throw error - } + try await client.deleteSession() + deleteCredentials() } // MARK: - Private @@ -145,6 +139,7 @@ extension ClientManager: VGClientDelegate { extension ClientManager { private func storeCredentials(username: String, password: String) { + deleteCredentials() if let passwordData = password.data(using: .utf8) { let keychainItem = [ kSecClass: kSecClassInternetPassword, @@ -183,13 +178,12 @@ extension ClientManager { } } - private func deleteCredentials(username: String) { + private func deleteCredentials() { let query = [ - kSecClass: kSecClassInternetPassword, - kSecAttrServer: Constants.keychainServer, - kSecAttrAccount: username + kSecClass: kSecClassInternetPassword ] as CFDictionary SecItemDelete(query) + print("Keychain deleting attempted") } } diff --git a/client-ios/Vapp/SettingsView.swift b/client-ios/Vapp/SettingsView.swift index 9f994e5..c1621a3 100644 --- a/client-ios/Vapp/SettingsView.swift +++ b/client-ios/Vapp/SettingsView.swift @@ -11,9 +11,9 @@ import PhotosUI struct SettingsView: View { @ObservedObject var homeViewModel: HomeViewModel @ObservedObject var viewModel = SettingsViewModel() - @Environment(\.dismiss) var dismiss + @Environment(\.dismiss) private var dismiss - @State var selectedPhoto: PhotosPickerItem? + @State private var selectedPhoto: PhotosPickerItem? var body: some View { NavigationStack { @@ -21,7 +21,7 @@ struct SettingsView: View { if viewModel.isLoading { ProgressView() } else { - PhotosPicker(selection: $selectedPhoto) { + PhotosPicker(selection: $selectedPhoto, matching: .images) { ZStack { if let url = viewModel.imageURL { AsyncImage(url: url) { phase in diff --git a/client-ios/Vapp/UsersView.swift b/client-ios/Vapp/UsersView.swift index 80fab1c..091adf7 100644 --- a/client-ios/Vapp/UsersView.swift +++ b/client-ios/Vapp/UsersView.swift @@ -16,6 +16,28 @@ struct UsersView: View { VStack { List(viewModel.users) { user in HStack { + if let url = user.imageURL { + AsyncImage(url: URL(string: url)) { phase in + if let image = phase.image { + image + .resizable() + .scaledToFill() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(8) + } else { + ProgressView() + .frame(width: 50, height: 50) + } + } + } else { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(8) + } Text(user.name) Spacer() Button(action: { From 4ae923b560ebff5c9b0af9191d2f219d09f03a42 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Tue, 14 May 2024 16:44:42 +0100 Subject: [PATCH 4/7] Fix incoming invite filtering --- client-ios/Vapp.xcodeproj/project.pbxproj | 2 +- client-ios/Vapp/Home/HomeViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client-ios/Vapp.xcodeproj/project.pbxproj b/client-ios/Vapp.xcodeproj/project.pbxproj index 4e82e39..c0507ca 100644 --- a/client-ios/Vapp.xcodeproj/project.pbxproj +++ b/client-ios/Vapp.xcodeproj/project.pbxproj @@ -445,7 +445,7 @@ repositoryURL = "https://github.com/Vonage/vonage-client-sdk-ios"; requirement = { kind = exactVersion; - version = 1.5.1; + version = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/client-ios/Vapp/Home/HomeViewModel.swift b/client-ios/Vapp/Home/HomeViewModel.swift index 8ee05e1..ab2cdf8 100644 --- a/client-ios/Vapp/Home/HomeViewModel.swift +++ b/client-ios/Vapp/Home/HomeViewModel.swift @@ -59,8 +59,8 @@ final class HomeViewModel: ObservableObject { clientManager.onEvent .receive(on: DispatchQueue.main) .filter { $0.kind == .memberInvited } - .filter { !($0.from is VGSystem) } .map { $0 as! VGMemberInvitedEvent } + .filter { $0.body.channel.id == nil } .sink { [weak self] event in guard event.body.user.name == self?.clientManager.user?.name else { return } self?.incomingConversationId = event.conversationId From 50868bab3e190981c14baaee029376d14b8e35d9 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Thu, 4 Jul 2024 12:08:54 +0100 Subject: [PATCH 5/7] Handle chat invites --- client-ios/Vapp/Helpers/ClientManager.swift | 1 + client-ios/Vapp/Home/HomeView.swift | 51 +++++++++++++++------ client-ios/Vapp/Home/HomeViewModel.swift | 18 ++++++-- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/client-ios/Vapp/Helpers/ClientManager.swift b/client-ios/Vapp/Helpers/ClientManager.swift index 455a6c0..c08b017 100644 --- a/client-ios/Vapp/Helpers/ClientManager.swift +++ b/client-ios/Vapp/Helpers/ClientManager.swift @@ -79,6 +79,7 @@ final class ClientManager: NSObject, ObservableObject { public func logout() async throws { try await client.deleteSession() + isAuthed = false deleteCredentials() } diff --git a/client-ios/Vapp/Home/HomeView.swift b/client-ios/Vapp/Home/HomeView.swift index 474e8a5..fa05324 100644 --- a/client-ios/Vapp/Home/HomeView.swift +++ b/client-ios/Vapp/Home/HomeView.swift @@ -24,22 +24,47 @@ struct HomeView: View { viewModel.showNewConversation = true }.buttonStyle(.bordered) } else { - List(viewModel.conversations) { conversation in - HStack { - Text(conversation.displayName ?? "") - Spacer() - } - .contentShape(Rectangle()) - .onTapGesture { - selectedConversation = conversation + List { + Section(header: Text("Conversations")) { + ForEach(viewModel.conversations[.joined] ?? []) { conversation in + HStack { + Text(conversation.displayName ?? "") + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + selectedConversation = conversation + } + .swipeActions { + Button("Delete") { + Task { + await viewModel.deleteConversation(conversation.id) + } + } + .tint(.red) + } + } } - .swipeActions { - Button("Delete") { - Task { - await viewModel.deleteConversation(conversation.id) + + Section(header: Text("Invites")) { + ForEach(viewModel.conversations[.invited] ?? []) { conversation in + HStack { + Text(conversation.displayName ?? "") + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + selectedConversation = conversation + } + .swipeActions { + Button("Decline") { + Task { + await viewModel.declineInvite(conversation.id) + } + } + .tint(.red) } } - .tint(.red) } } .refreshable { diff --git a/client-ios/Vapp/Home/HomeViewModel.swift b/client-ios/Vapp/Home/HomeViewModel.swift index ab2cdf8..8d16e8f 100644 --- a/client-ios/Vapp/Home/HomeViewModel.swift +++ b/client-ios/Vapp/Home/HomeViewModel.swift @@ -14,7 +14,7 @@ final class HomeViewModel: ObservableObject { private var subscriptions = Set() @Published var isLoading = false - @Published var conversations: [VGConversation] = [] + @Published var conversations: [VGMemberState: [VGConversation]] = [:] @Published var showNewConversation = false @Published var showIncomingCall = false @@ -78,7 +78,9 @@ final class HomeViewModel: ObservableObject { func loadConversations() async { do { let conversationPage = try await clientManager.client.getConversations() - conversations = conversationPage.conversations.filter { !$0.name.contains("NAM") } + let ungroupedConversations = conversationPage.conversations + .filter { !$0.name.contains("NAM") } + conversations = Dictionary(grouping: ungroupedConversations, by: { $0.memberState }) } catch { errorContainer = (true, error.localizedDescription) } @@ -88,7 +90,17 @@ final class HomeViewModel: ObservableObject { func deleteConversation(_ id: String) async { do { try await clientManager.client.deleteConversation(id) - conversations.removeAll { $0.id == id } + conversations[.joined]?.removeAll { $0.id == id } + } catch { + errorContainer = (true, error.localizedDescription) + } + } + + @MainActor + func declineInvite(_ id: String) async { + do { + try await clientManager.client.leaveConversation(id) + conversations[.invited]?.removeAll { $0.id == id } } catch { errorContainer = (true, error.localizedDescription) } From 13a12868be544aa4bf6b4521e94bcb1fb325ac65 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Fri, 12 Jul 2024 15:27:44 +0100 Subject: [PATCH 6/7] remove name from create conversation --- client-ios/Vapp/CreateConversationView.swift | 6 ++---- client-ios/Vapp/Home/HomeViewModel.swift | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client-ios/Vapp/CreateConversationView.swift b/client-ios/Vapp/CreateConversationView.swift index 15e7e91..bca384d 100644 --- a/client-ios/Vapp/CreateConversationView.swift +++ b/client-ios/Vapp/CreateConversationView.swift @@ -20,7 +20,6 @@ struct CreateConversationView: View { } else { Form { Section(header: Text("Conversation Details")) { - TextField("Name", text: $viewModel.name) TextField("Display Name", text: $viewModel.displayName) } @@ -50,7 +49,6 @@ final class NewConversationViewModel: ObservableObject { @Published var isLoading = false @Published var conversationCreated = false - @Published var name = "" @Published var displayName = "" @Published var errorContainer = (hasError: false, text: "") @@ -59,12 +57,12 @@ final class NewConversationViewModel: ObservableObject { @MainActor func createConversation() async { - if (name.isEmpty || displayName.isEmpty) { + if (displayName.isEmpty) { errorContainer = (true, "Please provide a conversation name and display name.") return } - let params = VGCreateConversationParameters(name: name, displayName: displayName) + let params = VGCreateConversationParameters(displayName: displayName) do { let convId = try await clientManager.client.createConversation(params) diff --git a/client-ios/Vapp/Home/HomeViewModel.swift b/client-ios/Vapp/Home/HomeViewModel.swift index 8d16e8f..1603ccc 100644 --- a/client-ios/Vapp/Home/HomeViewModel.swift +++ b/client-ios/Vapp/Home/HomeViewModel.swift @@ -79,7 +79,7 @@ final class HomeViewModel: ObservableObject { do { let conversationPage = try await clientManager.client.getConversations() let ungroupedConversations = conversationPage.conversations - .filter { !$0.name.contains("NAM") } + .filter { !($0.displayName?.isEmpty ?? false) } conversations = Dictionary(grouping: ungroupedConversations, by: { $0.memberState }) } catch { errorContainer = (true, error.localizedDescription) From b8ed5788adfa6c3fcd0f836767404174fa7321d1 Mon Sep 17 00:00:00 2001 From: Abdulhakim Ajetunmobi Date: Tue, 30 Jul 2024 16:07:56 +0100 Subject: [PATCH 7/7] Update project.pbxproj --- client-ios/Vapp.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-ios/Vapp.xcodeproj/project.pbxproj b/client-ios/Vapp.xcodeproj/project.pbxproj index c0507ca..9e56f44 100644 --- a/client-ios/Vapp.xcodeproj/project.pbxproj +++ b/client-ios/Vapp.xcodeproj/project.pbxproj @@ -445,7 +445,7 @@ repositoryURL = "https://github.com/Vonage/vonage-client-sdk-ios"; requirement = { kind = exactVersion; - version = 1.6.0; + version = 1.6.2; }; }; /* End XCRemoteSwiftPackageReference section */