diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index f7d1a12..b6152cb 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -8,6 +8,16 @@ /* Begin PBXBuildFile section */ 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */; }; + 89091993277D958D0025F4BB /* ConvoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091992277D958D0025F4BB /* ConvoViewModel.swift */; }; + 89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091994277DD9F80025F4BB /* ConvoFooterView.swift */; }; + 89091997277DDE030025F4BB /* ProfilePicturesOverlappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */; }; + 89091999277EFDCC0025F4BB /* leaveSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 89091998277EFDCC0025F4BB /* leaveSound.mp3 */; }; + 8909199C277EFDF30025F4BB /* joinSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 8909199B277EFDF30025F4BB /* joinSound.mp3 */; }; + 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */ = {isa = PBXBuildFile; productRef = 890C245C277BF628009B4A30 /* AgoraRtcKit */; }; + 890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C245F277BF896009B4A30 /* AgoraViewController.swift */; }; + 890C2462277BF935009B4A30 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2461277BF935009B4A30 /* CallView.swift */; }; + 890C2464277BFA97009B4A30 /* AgroRep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2463277BFA97009B4A30 /* AgroRep.swift */; }; + 890C2466277C5711009B4A30 /* AgoraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2465277C5711009B4A30 /* AgoraService.swift */; }; 891060E4277359B900F14509 /* CloudStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891060E3277359B900F14509 /* CloudStorageService.swift */; }; 891060E82774460500F14509 /* FirebaseInAppMessaging-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 891060E72774460500F14509 /* FirebaseInAppMessaging-Beta */; }; 891060EA2774460500F14509 /* FirebaseInAppMessagingSwift-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 891060E92774460500F14509 /* FirebaseInAppMessagingSwift-Beta */; }; @@ -39,6 +49,8 @@ 896CE7052770AE5D00DB474E /* InboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE7042770AE5D00DB474E /* InboxView.swift */; }; 896CE70727711BAB00DB474E /* FindFriendsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE70627711BAB00DB474E /* FindFriendsView.swift */; }; 896CE70927712CED00DB474E /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE70827712CED00DB474E /* StringExtension.swift */; }; + 89814683277D83F300DA46CD /* ConvosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89814682277D83F300DA46CD /* ConvosView.swift */; }; + 89814685277D857300DA46CD /* Convo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89814684277D857300DA46CD /* Convo.swift */; }; 89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89A107E4277476CC00B971FD /* PushNotificationService.swift */; }; 89A107E927753AB600B971FD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 89A107E827753AB500B971FD /* GoogleService-Info.plist */; }; 89AD5573276DD790001658E0 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AD5572276DD790001658E0 /* SplashView.swift */; }; @@ -85,6 +97,15 @@ /* Begin PBXFileReference section */ 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingOneView.swift; sourceTree = ""; }; + 89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = ""; }; + 89091994277DD9F80025F4BB /* ConvoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoFooterView.swift; sourceTree = ""; }; + 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = ""; }; + 89091998277EFDCC0025F4BB /* leaveSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = leaveSound.mp3; sourceTree = ""; }; + 8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = ""; }; + 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; + 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; + 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; + 890C2465277C5711009B4A30 /* AgoraService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraService.swift; sourceTree = ""; }; 891060E3277359B900F14509 /* CloudStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudStorageService.swift; sourceTree = ""; }; 891060E527743FEB00F14509 /* nirvana-ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "nirvana-ios.entitlements"; sourceTree = ""; }; 89110713277166D6005797FA /* UserFriends.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriends.swift; sourceTree = ""; }; @@ -116,6 +137,8 @@ 896CE7042770AE5D00DB474E /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = ""; }; 896CE70627711BAB00DB474E /* FindFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindFriendsView.swift; sourceTree = ""; }; 896CE70827712CED00DB474E /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + 89814682277D83F300DA46CD /* ConvosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvosView.swift; sourceTree = ""; }; + 89814684277D857300DA46CD /* Convo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Convo.swift; sourceTree = ""; }; 89A107E4277476CC00B971FD /* PushNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationService.swift; sourceTree = ""; }; 89A107E827753AB500B971FD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 89AD5572276DD790001658E0 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; @@ -148,6 +171,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */, 89BCABE2276B3CB0009E9B10 /* NavigationStack in Frameworks */, 89F18AD4276E881800115B1E /* FirebaseAuthCombine-Community in Frameworks */, 89F18AEA276E881900115B1E /* FirebaseStorageSwift-Beta in Frameworks */, @@ -174,6 +198,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 8909199A277EFDE50025F4BB /* sounds */ = { + isa = PBXGroup; + children = ( + 8909199B277EFDF30025F4BB /* joinSound.mp3 */, + 89091998277EFDCC0025F4BB /* leaveSound.mp3 */, + ); + path = sounds; + sourceTree = ""; + }; + 890C245E277BF888009B4A30 /* VoiceCallTest */ = { + isa = PBXGroup; + children = ( + 890C245F277BF896009B4A30 /* AgoraViewController.swift */, + 890C2461277BF935009B4A30 /* CallView.swift */, + 890C2463277BFA97009B4A30 /* AgroRep.swift */, + ); + path = VoiceCallTest; + sourceTree = ""; + }; 891060E62774460500F14509 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -201,6 +244,7 @@ 891A15DF27653A7000B26659 /* nirvana-ios */ = { isa = PBXGroup; children = ( + 8909199A277EFDE50025F4BB /* sounds */, 891060E527743FEB00F14509 /* nirvana-ios.entitlements */, 89AD557D276E01E2001658E0 /* Repositories */, 89F05BA2276ACBFA0002E9C7 /* Services */, @@ -237,6 +281,7 @@ 891A15F62765639B00B26659 /* Views */ = { isa = PBXGroup; children = ( + 89814681277D83CF00DA46CD /* Convos */, 896CE7032770AE3400DB474E /* Inbox */, 894E80BF276EFCF500624B72 /* AudioPlayer */, 89F18AF3276ECF1800115B1E /* AddBasicInfo */, @@ -305,6 +350,7 @@ 89BDCDF02769E64200577829 /* Message.swift */, 891A160D27657CEA00B26659 /* User.swift */, 89110713277166D6005797FA /* UserFriends.swift */, + 89814684277D857300DA46CD /* Convo.swift */, ); path = Models; sourceTree = ""; @@ -343,6 +389,17 @@ path = Inbox; sourceTree = ""; }; + 89814681277D83CF00DA46CD /* Convos */ = { + isa = PBXGroup; + children = ( + 89814682277D83F300DA46CD /* ConvosView.swift */, + 89091992277D958D0025F4BB /* ConvoViewModel.swift */, + 89091994277DD9F80025F4BB /* ConvoFooterView.swift */, + 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */, + ); + path = Convos; + sourceTree = ""; + }; 89AD5571276DD77D001658E0 /* SplashView */ = { isa = PBXGroup; children = ( @@ -372,6 +429,7 @@ 89BCABDF276B3588009E9B10 /* InnerCircle */ = { isa = PBXGroup; children = ( + 890C245E277BF888009B4A30 /* VoiceCallTest */, 89BCABE4276B3D58009E9B10 /* InnerCircleView.swift */, 89BCABE6276B3D64009E9B10 /* InnerCircleViewModel.swift */, 896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */, @@ -416,6 +474,7 @@ 89F05BA3276ACC060002E9C7 /* ContactService.swift */, 89B46E70276893E200B74255 /* AuthSessionStore.swift */, 89A107E4277476CC00B971FD /* PushNotificationService.swift */, + 890C2465277C5711009B4A30 /* AgoraService.swift */, ); path = Services; sourceTree = ""; @@ -490,6 +549,7 @@ 891060E72774460500F14509 /* FirebaseInAppMessaging-Beta */, 891060E92774460500F14509 /* FirebaseInAppMessagingSwift-Beta */, 891060EB2774460500F14509 /* FirebaseMessaging */, + 890C245C277BF628009B4A30 /* AgoraRtcKit */, ); productName = "nirvana-ios"; productReference = 891A15DD27653A7000B26659 /* nirvana-ios.app */; @@ -523,6 +583,7 @@ 89F1386E2767F55F006680ED /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, 89BCABE0276B3CB0009E9B10 /* XCRemoteSwiftPackageReference "swiftui-navigation-stack" */, 89F18ACC276E881800115B1E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 890C245B277BF628009B4A30 /* XCRemoteSwiftPackageReference "AgoraAudio_iOS" */, ); productRefGroup = 891A15DE27653A7000B26659 /* Products */; projectDirPath = ""; @@ -540,7 +601,9 @@ files = ( 891A15F12765528700B26659 /* Satisfy-Regular.ttf in Resources */, 89A107E927753AB600B971FD /* GoogleService-Info.plist in Resources */, + 89091999277EFDCC0025F4BB /* leaveSound.mp3 in Resources */, 891A15E827653A7100B26659 /* Preview Assets.xcassets in Resources */, + 8909199C277EFDF30025F4BB /* joinSound.mp3 in Resources */, 891A15E527653A7100B26659 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -556,12 +619,16 @@ 891060E4277359B900F14509 /* CloudStorageService.swift in Sources */, 896CE7002770847400DB474E /* CircleFooterView.swift in Sources */, 894E80BE276ED7A700624B72 /* avatars.swift in Sources */, + 89091997277DDE030025F4BB /* ProfilePicturesOverlappedView.swift in Sources */, + 890C2462277BF935009B4A30 /* CallView.swift in Sources */, 896CE7052770AE5D00DB474E /* InboxView.swift in Sources */, 891A160327656FC700B26659 /* HomeView.swift in Sources */, 896CE70927712CED00DB474E /* StringExtension.swift in Sources */, 89110714277166D6005797FA /* UserFriends.swift in Sources */, 896CE70727711BAB00DB474E /* FindFriendsView.swift in Sources */, 891A160B2765732700B26659 /* HomeViewModel.swift in Sources */, + 89814685277D857300DA46CD /* Convo.swift in Sources */, + 89814683277D83F300DA46CD /* ConvosView.swift in Sources */, 89BCABEB276D2A09009E9B10 /* PhoneVerificationView.swift in Sources */, 892179E62765FECD001E6C60 /* FooterControlsViewModel.swift in Sources */, 891A15F5276557AB00B26659 /* colors.swift in Sources */, @@ -574,10 +641,12 @@ 89BDCDEF2769B50C00577829 /* ContactsView.swift in Sources */, 891A1611276590B000B26659 /* ChatsCarouselView.swift in Sources */, 89BDCDE527695DB900577829 /* NirvanaImage.swift in Sources */, + 890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */, 891A16132765A44300B26659 /* ChatsCarouselViewModel.swift in Sources */, 89F05B9B276A87920002E9C7 /* ContactsViewModel.swift in Sources */, 896CE6FE27707E6B00DB474E /* CircleNavigationView.swift in Sources */, 89AD5573276DD790001658E0 /* SplashView.swift in Sources */, + 890C2466277C5711009B4A30 /* AgoraService.swift in Sources */, 89F18AEC276E92BF00115B1E /* CustomExtensions.swift in Sources */, 89F18AF1276ECAA300115B1E /* RouterView.swift in Sources */, 89F18AEE276EB55C00115B1E /* ServiceState.swift in Sources */, @@ -586,6 +655,7 @@ 89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */, 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */, 896CE7022770892F00DB474E /* CircleGridView.swift in Sources */, + 89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */, 89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */, 89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */, 89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */, @@ -597,8 +667,10 @@ 89BCABE5276B3D58009E9B10 /* InnerCircleView.swift in Sources */, 894E80C1276EFD0500624B72 /* AudioPlayerView.swift in Sources */, 89F18AF5276ECF2A00115B1E /* AddBasicInfoView.swift in Sources */, + 89091993277D958D0025F4BB /* ConvoViewModel.swift in Sources */, 89BCABF0276D53C4009E9B10 /* PhoneVerificationViewModel.swift in Sources */, 89BDCDE72769616600577829 /* UIImage+Extension.swift in Sources */, + 890C2464277BFA97009B4A30 /* AgroRep.swift in Sources */, 89F1386D2767EB19006680ED /* SignInView.swift in Sources */, 89BDCDF12769E64200577829 /* Message.swift in Sources */, 891A160E27657CEA00B26659 /* User.swift in Sources */, @@ -822,6 +894,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 890C245B277BF628009B4A30 /* XCRemoteSwiftPackageReference "AgoraAudio_iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AgoraIO/AgoraAudio_iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.5.0; + }; + }; 89BCABE0276B3CB0009E9B10 /* XCRemoteSwiftPackageReference "swiftui-navigation-stack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/matteopuc/swiftui-navigation-stack.git"; @@ -849,6 +929,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 890C245C277BF628009B4A30 /* AgoraRtcKit */ = { + isa = XCSwiftPackageProductDependency; + package = 890C245B277BF628009B4A30 /* XCRemoteSwiftPackageReference "AgoraAudio_iOS" */; + productName = AgoraRtcKit; + }; 891060E72774460500F14509 /* FirebaseInAppMessaging-Beta */ = { isa = XCSwiftPackageProductDependency; package = 89F18ACC276E881800115B1E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5bb82bd..f7e591c 100644 --- a/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": "0.20200225.4" } }, + { + "package": "AgoraRtcKit", + "repositoryURL": "https://github.com/AgoraIO/AgoraAudio_iOS", + "state": { + "branch": null, + "revision": "74856715c8d8b381b0dbd02ddc86fbbfd0f0f6c4", + "version": "3.6.0" + } + }, { "package": "AppAuth", "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", diff --git a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme new file mode 100644 index 0000000..911ea35 --- /dev/null +++ b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme new file mode 100644 index 0000000..e692ea2 --- /dev/null +++ b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist b/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist index a6053c8..72de430 100644 --- a/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,14 +9,14 @@ isShown orderHint - 3 + 2 Promises (Playground) 2.xcscheme isShown orderHint - 4 + 3 Promises (Playground) 3.xcscheme @@ -46,10 +46,25 @@ orderHint 0 - nirvana-ios.xcscheme_^#shared#^_ + nirvana-ios-dev.xcscheme_^#shared#^_ orderHint - 0 + 4 + + nirvana-ios.xcscheme_^#shared#^_ + + isShown + + orderHint + 1 + + + SuppressBuildableAutocreation + + 891A15DC27653A7000B26659 + + primary + diff --git a/nirvana-ios/Assets.xcassets/sounds/Contents.json b/nirvana-ios/Assets.xcassets/sounds/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/nirvana-ios/Assets.xcassets/sounds/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json new file mode 100644 index 0000000..58de6d7 --- /dev/null +++ b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "joinSound.mp3", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/joinSound.mp3 b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/joinSound.mp3 new file mode 100644 index 0000000..4a42d9b Binary files /dev/null and b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/joinSound.mp3 differ diff --git a/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json new file mode 100644 index 0000000..8ec16f8 --- /dev/null +++ b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "leaveSound.mp3", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/leaveSound.mp3 b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/leaveSound.mp3 new file mode 100644 index 0000000..ef17270 Binary files /dev/null and b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/leaveSound.mp3 differ diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift new file mode 100644 index 0000000..cd75d6e --- /dev/null +++ b/nirvana-ios/Models/Convo.swift @@ -0,0 +1,40 @@ +// +// Convo.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import FirebaseFirestoreSwift +import FirebaseFirestore + +enum ConvoState: String, Codable { + case initialized + case active + case complete +} + +struct Convo: Identifiable, Codable { + // channel name for agora's purposes + @DocumentID var id: String? + + var leaderUserId: String + var receiverUserId: String + + var agoraToken: String + + var state: ConvoState + + var users: [String] = [] + + @ServerTimestamp var startedTimestamp: Date? + var endedTimestamp: Date? + var receiverEndedTimestamp: Date? +} + +//struct DetailConvo: Convo { +// func getRelativeStartTime() { +// +// } +//} diff --git a/nirvana-ios/Models/User.swift b/nirvana-ios/Models/User.swift index c824f3a..54e2926 100644 --- a/nirvana-ios/Models/User.swift +++ b/nirvana-ios/Models/User.swift @@ -14,6 +14,7 @@ enum UserStatus: String, Codable { case online case offline case background + case inConvo } struct User: Identifiable, Codable { @@ -26,6 +27,7 @@ struct User: Identifiable, Codable { var userStatus: UserStatus? + var lastUpdatedStatusTimestamp: Date? @ServerTimestamp var lastLoggedInTimestamp: Date? @ServerTimestamp var createdTimestamp: Date? diff --git a/nirvana-ios/Repositories/FirestoreService.swift b/nirvana-ios/Repositories/FirestoreService.swift index 9a7b480..59e1aad 100644 --- a/nirvana-ios/Repositories/FirestoreService.swift +++ b/nirvana-ios/Repositories/FirestoreService.swift @@ -16,6 +16,7 @@ class FirestoreService { case users = "users" case messages = "messages" case userFriends = "user_friends" // associating a user to + case convos = "convos" } private var db = Firestore.firestore() @@ -175,7 +176,7 @@ extension FirestoreService { func updateUserStatus(userId: String, userStatus: UserStatus, completion: @escaping((_ state: ServiceState) -> ())) { do { - let _ = try db.collection(Collection.users.rawValue).document(userId).setData(["userStatus": userStatus.rawValue], merge: true) + let _ = try db.collection(Collection.users.rawValue).document(userId).setData(["userStatus": userStatus.rawValue, "lastUpdatedStatusTimestamp": getFirestoreServerTimestamp()], merge: true) completion(ServiceState.success("Updated user status")) } catch { print("error in updating user \(error.localizedDescription)") @@ -183,3 +184,46 @@ extension FirestoreService { } } } + + +// convos +extension FirestoreService { + func getConvo(channelName: String, completion: @escaping((_ user: Convo?) -> ())) { + let docRef = db.collection(Collection.convos.rawValue).document(channelName) + + docRef.getDocument { (document, error) in + if let document = document, document.exists { + let returnedConvo = try? document.data(as: Convo.self) + completion(returnedConvo) + } else { + print("convo doesn't exist") + completion(nil) + } + } + } + + func createConvo(convo: Convo, completion: @escaping((_ state: ServiceState) -> ())) { + do { + let _ = try db.collection(Collection.convos.rawValue).document(convo.id!).setData(from: convo) + + completion(ServiceState.success("convo created")) + } catch { + print("error in creating convo \(error.localizedDescription)") + completion(ServiceState.error(ServiceError(description: error.localizedDescription))) + } + } + + func updateConvo(convo: Convo, completion: @escaping((_ state: ServiceState) -> ())) { + do { + if convo.id != nil { + let _ = try db.collection(Collection.convos.rawValue).document(convo.id!).setData(from: convo) + completion(ServiceState.success("Updated convo in firestore service")) + } else { + completion(ServiceState.error(ServiceError(description: "No convo id given to firestore service"))) + } + } catch { + print("error in updating convo \(error.localizedDescription)") + completion(ServiceState.error(ServiceError(description: error.localizedDescription))) + } + } +} diff --git a/nirvana-ios/Services/AgoraService.swift b/nirvana-ios/Services/AgoraService.swift new file mode 100644 index 0000000..dc400c5 --- /dev/null +++ b/nirvana-ios/Services/AgoraService.swift @@ -0,0 +1,75 @@ +// +// AgoraService.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import FirebaseAuth +import Firebase + +class AgoraService { + lazy var functions = Functions.functions() + + func getAgoraUserTokenServer(channelName: String) { + let currentUser = Auth.auth().currentUser + currentUser?.getIDTokenForcingRefresh(true) { idToken, error in + if let error = error { + // Handle error + return; + } + + // Send token to your backend via HTTPS + // ... + let baseUrl = ProcessInfo.processInfo.environment["nirvana-server-url"] ?? "http://localhost:3000" + let reqUrl = baseUrl + "agoraToken?channelName=" + channelName + guard let url = URL(string: reqUrl) else { fatalError("Missing URL") } + + var urlRequest = URLRequest(url: url) + urlRequest.addValue(idToken!, forHTTPHeaderField: "authorization") + urlRequest.httpMethod = "GET" + + let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in + if let error = error { + print("Request error: ", error) + return + } + + guard let response = response as? HTTPURLResponse else { return } + + if response.statusCode == 200 { + guard let data = data else { return } + print("the token is: \(data)") + } + } + + dataTask.resume() + } + } + + func getAgoraTokenCF(channelName: String, completion: @escaping((_ token: String?) -> ())) { + // validate channelName + + + functions.httpsCallable("agoraToken").call(["channelName": channelName]) {[weak self] result, error in + if let error = error as NSError? { + if error.domain == FunctionsErrorDomain { + let code = FunctionsErrorCode(rawValue: error.code) + let message = error.localizedDescription + let details = error.userInfo[FunctionsErrorDetailsKey] + } + print(error) + completion(nil) + } + + print(result) + if let data = result?.data as? [String: Any], let token = data["token"] as? String { + print("got agora token from cloud function") + completion(token) + } else { + completion(nil) + } + } + } +} diff --git a/nirvana-ios/Services/AuthSessionStore.swift b/nirvana-ios/Services/AuthSessionStore.swift index f60989a..cbc2740 100644 --- a/nirvana-ios/Services/AuthSessionStore.swift +++ b/nirvana-ios/Services/AuthSessionStore.swift @@ -140,7 +140,7 @@ final class AuthSessionStore: ObservableObject, SessionStore { } } - func getCurrentUserId() -> String? { + static func getCurrentUserId() -> String? { return Auth.auth().currentUser?.uid } @@ -214,7 +214,7 @@ extension AuthSessionStore { } // can't get proper listeners working if don't have current user's id - let currUserId = self.getCurrentUserId() + let currUserId = AuthSessionStore.getCurrentUserId() if currUserId == nil { print("no user id available to initiate data listeners") return @@ -457,7 +457,7 @@ extension AuthSessionStore { print("user already has status: \(userStatus)") } else { - if let uid = self.getCurrentUserId() { + if let uid = AuthSessionStore.getCurrentUserId() { self.firestoreService.updateUserStatus(userId: uid, userStatus: userStatus) {res in print(res) } diff --git a/nirvana-ios/Views/Contacts/FindFriendsView.swift b/nirvana-ios/Views/Contacts/FindFriendsView.swift index bf992f9..44e7d7d 100644 --- a/nirvana-ios/Views/Contacts/FindFriendsView.swift +++ b/nirvana-ios/Views/Contacts/FindFriendsView.swift @@ -135,7 +135,7 @@ struct ListContactRow: View { secondaryButton: .default(Text("Confirm")) { print("adding contact to circle") // call method in vm to get it done, then navigate to the circle - if self.authSessionStore.friendsArr.count < 10 { + if self.authSessionStore.friendsArr.count < 10 || self.authSessionStore.user?.phoneNumber == "+19499230445" { self.contactsVM.addOrActivateFriendToCircle(userId: self.authSessionStore.user!.id!, friendId: (contact.user!.id)!) {res in print(res) diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift new file mode 100644 index 0000000..98b645b --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -0,0 +1,127 @@ +// +// ConvoFooterView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/30/21. +// + +import SwiftUI + +struct ConvoFooterView: View { + @EnvironmentObject var convoVM: ConvoViewModel + + @State private var convoRelativeTime = "started 20 minutes ago" + + var body: some View { + ZStack(alignment:.bottomTrailing) { + VStack(alignment:.center) { + Spacer() + + HStack { + ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35)) + .padding(.leading, 20) + .padding(.trailing, 10) + + + // meta data of convo + VStack (alignment: .leading) { + Text("kev, sarth, and 5 others") + .font(.footnote) + .foregroundColor(NirvanaColor.light) + + switch self.convoVM.connectionState { + case .connecting: + Label("connecting", systemImage: "chart.bar") + .foregroundColor(Color.orange) + .font(.caption) + case .connected: + Label("connected", systemImage: "chart.bar.fill") + .foregroundColor(Color.green) + .font(.caption) + case .reconnecting: + Label("reconnecting", systemImage: "chart.bar") + .foregroundColor(Color.orange) + .font(.caption) + case .failed: + Label("failed", systemImage: "chart.bar") + .foregroundColor(Color.red) + .font(.caption) + case .disconnected: + Label("disconnected", systemImage: "chart.bar") + .foregroundColor(Color.red) + .font(.caption) + default: + Label("disconnected", systemImage: "chart.bar") + .foregroundColor(Color.red) + .font(.caption) + } + + Text(self.convoRelativeTime) + .font(.caption2) + .foregroundColor(.gray) + } + + Spacer() + + // disconnect button + Button { + print("showing more info about the chat") + + // show a sheet of the members in the call and add them if I want + + } label: { + Label("attendees", systemImage: "person.2.circle.fill") + .labelStyle(.iconOnly) + .foregroundColor(NirvanaColor.teal) + .font(.title2) + .padding(.trailing, 5) + } + + // disconnect button + Button { + print("disconnecting user now ") + + self.convoVM.leaveConvo() + + // show success or failure toast + } label: { + Label("Call", systemImage: "powerplug.fill") + .labelStyle(.iconOnly) + .foregroundColor(Color.orange) + .font(.title2) + .padding(.trailing, 5) + } + + } + .frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle + .background(Color.white.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous)) + .shadow(color: Color.black.opacity(0.25), radius: 30, x: 0, y: 20) + } + } + .padding() + .animation(Animation.spring(), value: self.convoVM.selectedConvoId) + .offset( + x:0, + y:self.convoVM.selectedConvoId == nil ? 150 : 0 + ) + .onChange(of: self.convoVM.selectedConvoId) {newConvo in + // update meta data based on which convo was selected + + // reset the selected options to start fresh +// self.convoRelativeTime = "" + + // get relative start time of convo +// let formatter = RelativeDateTimeFormatter() +// formatter.unitsStyle = .full +// +// let relativeDate = formatter.localizedString(for: ?.sentTimestamp ?? Date(), relativeTo: Date()) + } + } +} + +struct ConvoFooterView_Previews: PreviewProvider { + static var previews: some View { + ConvoFooterView() + } +} diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift new file mode 100644 index 0000000..44da5ed --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -0,0 +1,426 @@ +// +// ConvoViewModel.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import AgoraRtcKit +import Firebase + +class ConvoViewModel: NSObject, ObservableObject { + @Published var selectedConvoId:String? = nil + @Published var connectionState: AgoraConnectionStateType? = nil + + let firestoreService = FirestoreService() + private var db = Firestore.firestore() + var convosListener: ListenerRegistration? = nil + + let agoraService = AgoraService() + // TODO: put the value in environment variables + lazy var agoraKit: AgoraRtcEngineKit? = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) + + private var allConvos: [Convo] = [] + @Published var relevantConvos: [Convo] = [] + + func isInCall() -> Bool { + return self.selectedConvoId != nil + } + + static var relevancyAcceptance = 0.6 + + override init() { + super.init() + + // start process of data collection + let userId = AuthSessionStore.getCurrentUserId() + if userId == nil { + print("no authenticated user") + } + + // get all active userfriends + var scopeUserIds: [String] = [] + scopeUserIds.append(userId!) + + db.collection("user_friends").whereField("userId", isEqualTo: userId).whereField("isActive", isEqualTo: true) + .getDocuments() {[weak self] (querySnapshot, err) in + if let err = err { + print("Error getting user's active friends: \(err)") + } else { + for document in querySnapshot!.documents { + let userFriend:UserFriends? = try? document.data(as: UserFriends.self) + + if userFriend == nil { + continue + } + + scopeUserIds.append(userFriend!.friendId) + } + + + //TODO: can only use 10 in array for firestore comparison...enact two listeners if need more than that + let splicedArray = Array(scopeUserIds.prefix(10)) + + print("the scope of users to search for convos is \(splicedArray)") + + // initiate convo listener to get all relevant convos + // any convo that is active + // and any of my active friends or I am in that convo + // if am being "called" catch that and join the convo/channel + self?.convosListener = self?.db.collection("convos").whereField("state", isEqualTo: ConvoState.active.rawValue).whereField("users", arrayContainsAny: splicedArray) + .addSnapshotListener { querySnapshot, error in + print("convos listener") + guard let documents = querySnapshot?.documents else { + print("Error fetching convos \(error!)") + return + } + + self?.allConvos.removeAll() + self?.relevantConvos.removeAll() + + for document in querySnapshot!.documents { + let convo:Convo? = try? document.data(as: Convo.self) + + if convo == nil { + continue + } + + // if convo receiver is me and I'm not in a call or this call already, then join in + if convo!.receiverUserId == userId && !(self?.isInCall())! + && convo!.receiverEndedTimestamp == nil { + self?.joinConvo(convo: convo!) + } + + // if I am in a convo in which the last/loner has left, I need to leave the convo + if convo!.receiverEndedTimestamp != nil && (self?.isInCall())! && convo!.users.contains(userId!) { + self?.leaveConvo() + } + + self?.allConvos.append(convo!) + } + + // only show convos in which I know a majority of the people inside + // use relevancyAcceptance value + self?.relevantConvos = (self?.allConvos.filter{convo in + var relevancyCount = 0 + // go through all users in this convo + for user in convo.users { + if scopeUserIds.contains(user) { + relevancyCount += 1 + } + } + + let relevancyScore = Double(relevancyCount / convo.users.count) + print("relevancy score: \(relevancyScore)") + + if relevancyScore > Self.relevancyAcceptance { + print("this convo counts as relevant!!!") + return true + } + return false + })! + } + } + } + } + + deinit { + // deinit the firestore listener + self.convosListener?.remove() + + // deinit the agora engine + print("deiniting and cleaning up convo view model/agora services") + + AgoraRtcEngineKit.destroy() + + self.leaveConvo() + } + + /** + @param: friendId: the second person in the convo...the receiver + **/ + func startConvo(friendId: String) { + // TODO: need to make sure the other user is online or already did in view + + // making sure this user is authenticated + if let userId = AuthSessionStore.getCurrentUserId() { + let channelName = UUID().uuidString + + // get a new agora token from cloud function + self.agoraService.getAgoraTokenCF(channelName: channelName) {[weak self] token in + if token == nil { + print("error in getting token") + return + } + + let _ = [userId] // just one user now, and that's me...but no one has joined yet + // only considered joined when I have officially joined the agora channel + + let convo = Convo(id: channelName, leaderUserId: userId, receiverUserId: friendId, agoraToken: token!, state: .initialized, users: [], startedTimestamp: nil, endedTimestamp: nil) + + // create a convo/channel in db to notify the other user with proper attributes + self?.firestoreService.createConvo(convo: convo) {[weak self] res in + switch res { + case .success: + // join the convo myself + self?.joinConvo(convo: convo) + case .error(let error): + print(error) + default: + return + } + } + } + } + } + + func joinConvo(convo: Convo) { + // ensure that this user leaves all other channels + if self.isInCall() { + self.leaveConvo() + } + + // update ui to select this one + self.selectedConvoId = convo.id + + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) + // finally join channel + self.agoraKit?.delegate = self + self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil) + } + + + /** + Allow user to join an active convo + */ + func joinConvo() { + // ensure the convo is active and includes 2 people in it currently...shouldn't be shown if not anyway + + // ensure that this user leaves all other channels + if self.isInCall() { + print("can't join another call, already in one") + return + } + + let convo = self.relevantConvos.first {convo in + convo.id == self.selectedConvoId + } + + if convo == nil { + // handles the offchance that the convo was deactivated or was mistakenly seen by this user + print("convo not available") + return + } + + // TODO: if the convo has more than 10 people, stop user from joining...that's too expensive... + + let convoAgoraToken:String = convo!.agoraToken + let channelName:String = convo!.id! + + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) + // finally join channel + self.agoraKit?.delegate = self + self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) + } + + // anyone can leave at any time + func leaveConvo() { + self.agoraKit?.delegate = self + agoraKit?.leaveChannel(nil) + + let userId = AuthSessionStore.getCurrentUserId() + + if userId == nil { + print("not authenticated...can't join channel") + return + } + + if self.selectedConvoId == nil { + print("not in a convo currently!") + return + } + + self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in + print(res) + + switch res { + case .success: + print("updated user status") + case .error(let error): + print(error) + default: + return + } + } + + self.firestoreService.getConvo(channelName: self.selectedConvoId!) {[weak self] convo in + if convo == nil { + print("no such convo found") + return + } + + var updatedConvo = convo + //update convo in database to show that I left...if I am the last person, also close out the channel + + // if I am the last one in the convo, then end the convo in the db + if updatedConvo!.users.count == 1 && updatedConvo!.users[0] == userId { + updatedConvo!.state = .complete + updatedConvo!.endedTimestamp = Date() + } // if I am second to last, the last guy is a loner + else if updatedConvo!.users.count == 2 && updatedConvo!.users.contains(userId!) { + updatedConvo!.receiverEndedTimestamp = Date() + } + + let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in + return arrUserId != userId + } + + updatedConvo!.users = updatedUsers + + self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in + self?.selectedConvoId = nil + + print("left convo officially in our db as well") + } + } + } + + func destroyInstance() { + AgoraRtcEngineKit.destroy() + } +} + +extension ConvoViewModel { + + func playJoinAudioEffect(engine: AgoraRtcEngineKit) { + print("playing JOINED audio sound") + + // Sets the audio effect ID. + let EFFECT_ID:Int32 = 1 + // Sets the path of the audio effect file. + let filePath = Bundle.main.path(forResource: "joinSound", ofType: "mp3") + // Sets the number of times the audio effect loops. -1 represents an infinite loop. + let loopCount = 1 + // Sets the pitch of the audio effect. The value range is 0.5 to 2.0, where 1.0 is the original pitch. + let pitch: Double = 1.0 + // Sets the spatial position of the audio effect. The value range is -1.0 to 1.0. + // -1.0 represents the audio effect occurs on the left; 0 represents the audio effect occurs in the front; 1.0 represents the audio effect occurs on the right. + let pan: Double = 1.0 + // Sets the volume of the audio effect. The value range is 0 to 100. 100 represents the original volume. + let gain = 100.0 + // Sets whether to publish the audio effect to the remote users. true represents that both the local user and remote users can hear the audio effect; false represents that only the local user can hear the audio effect. + let publish = false + // Sets the playback position (ms) of the audio effect file. 500 represents that the playback starts at the 500 ms mark of the audio effect file. + let startPos: Int32 = 500; + // Plays the specified audio effect file. + engine.playEffect(EFFECT_ID, filePath: filePath, loopCount: Int32(loopCount), pitch: pitch, pan: pan, gain: gain, publish: publish, startPos: startPos) + } + + func playLeaveAudioEffect(engine: AgoraRtcEngineKit) { + print("playing LEFT audio sound") + + // Sets the audio effect ID. + let EFFECT_ID:Int32 = 2 + // Sets the path of the audio effect file. + let filePath = Bundle.main.path(forResource: "leaveSound", ofType: "mp3") + // Sets the number of times the audio effect loops. -1 represents an infinite loop. + let loopCount = 1 + // Sets the pitch of the audio effect. The value range is 0.5 to 2.0, where 1.0 is the original pitch. + let pitch: Double = 1.0 + // Sets the spatial position of the audio effect. The value range is -1.0 to 1.0. + // -1.0 represents the audio effect occurs on the left; 0 represents the audio effect occurs in the front; 1.0 represents the audio effect occurs on the right. + let pan: Double = 1.0 + // Sets the volume of the audio effect. The value range is 0 to 100. 100 represents the original volume. + let gain = 100.0 + // Sets whether to publish the audio effect to the remote users. true represents that both the local user and remote users can hear the audio effect; false represents that only the local user can hear the audio effect. + let publish = false + // Sets the playback position (ms) of the audio effect file. 500 represents that the playback starts at the 500 ms mark of the audio effect file. + let startPos: Int32 = 500; + // Plays the specified audio effect file. + engine.playEffect(EFFECT_ID, filePath: filePath, loopCount: Int32(loopCount), pitch: pitch, pan: pan, gain: gain, publish: publish, startPos: startPos) + } +} + +extension ConvoViewModel: AgoraRtcEngineDelegate { + func rtcEngine(_ engine: AgoraRtcEngineKit, connectionChangedTo state: AgoraConnectionStateType, reason: AgoraConnectionChangedReason) { + self.connectionState = state + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + print("I did join channel") + + self.playJoinAudioEffect(engine: engine) + + // TODO: leave channel if it's just me + + let userId = AuthSessionStore.getCurrentUserId() + + if userId == nil { + print("not authenticated...can't join channel") + return + } + + self.firestoreService.getConvo(channelName: channel) {convo in + if convo == nil { + print("no such convo found") + return + } + + var updatedConvo = convo + + // set my user status to inConvo + self.firestoreService.updateUserStatus(userId: userId!, userStatus: .inConvo) {[weak self] res in + print(res) + + switch res { + case .success: + // update convo in database users array to let them know I am in now + + updatedConvo!.users.append(userId!) + updatedConvo!.state = .active + + self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in + print(res) + + } + case .error(let error): + print(error) + default: + return + } + } + } + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { + print("I did leave channel") + + // sound effect upon leaving + self.playLeaveAudioEffect(engine: engine) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + print("new user joined \(uid)") + + self.playJoinAudioEffect(engine: engine) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + print("a user left") + + self.playLeaveAudioEffect(engine: engine) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + print("did occur warning: \(warningCode.rawValue)") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + print("did occur error: \(errorCode.rawValue)") + + // publish an error in view...toast or something + + self.selectedConvoId = nil + } +} diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift new file mode 100644 index 0000000..d5dff04 --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -0,0 +1,76 @@ +// +// ConvosView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import SwiftUI + +struct ConvosView: View { + @EnvironmentObject var vm: ConvoViewModel + @State var animate = false + + var body: some View { + ScrollView([.horizontal]) { + HStack { + ForEach(0.. 0 { scrollReaderValue.scrollTo(self.selectedFriendIndex) } + + self.animateLiveConvos = true } } // scrollview reader } @@ -283,7 +351,7 @@ struct CircleGridView: View { return false } - private func getBubbleTint(friendIndex: Int, friendDbId: String) -> Color { + private func getBubbleTint(friendDbId: String) -> Color { // TODO: check if I listened to the message before or not if self.haveNewMessageFromFriend(friendDbId: friendDbId) { // this friend has a message for me return Color.orange.opacity(0.8) @@ -295,6 +363,14 @@ struct CircleGridView: View { return NirvanaColor.dimTeal.opacity(0.2) } + + private func getBubbleTint(convoId: String?) -> Color { + if convoId != nil && convoId == self.convoVM.selectedConvoId { + return NirvanaColor.teal.opacity(0.7) + } + + return NirvanaColor.dimTeal.opacity(0.2) + } } struct CircleGridView_Previews: PreviewProvider { @@ -309,12 +385,16 @@ struct CircleGridView_Previews: PreviewProvider { extension CircleGridView { // getting the proxy of an individual item // and decoding into a scale that the item should take - private func getScale(proxy: GeometryProxy, itemNumber: Int, userId: String?) -> CGFloat { + private func getScale(proxy: GeometryProxy, itemNumber: Int, userId: String? = nil, convoId: String? = nil) -> CGFloat { // if this user is selected if userId != nil && userId == self.selectedFriendIndex { return big + 0.2 } + if convoId != nil && convoId == self.convoVM.selectedConvoId { + return big + 0.2 + } + // if this user that we are rendering // has a new message for us // TODO: too much memory usage doing this across the board @@ -377,10 +457,16 @@ extension CircleGridView { extension CircleGridView { // listening to messages private func handleTap(gridItemIndex: Int, friendId: String) { - // TODO: if there is a message, then listen to it - // then after that, check if friend is online, and if so, then connect with him/her if they are free/not in another call print("tap gesture activated") + // if friend and I are online, start call immediately + if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online + && self.authSessionStore.user?.userStatus == .online { + self.convoVM.startConvo(friendId: friendId) + + return + } + // clearing the player to make room for this friend's convo or to deselect this user self.queuePlayer.removeAllItems() diff --git a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift index 0255d52..412f3d6 100644 --- a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift @@ -83,15 +83,21 @@ struct CircleNavigationView: View { .foregroundColor(Color.green) case .offline: Circle() - default: + .frame(width: 10, height: 10) + .foregroundColor(Color.red) + case .inConvo: Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.orange) + default: + EmptyView() } } } else { Image((self.authSessionStore.user?.avatar)!) .resizable() .scaledToFit() - .background(self.innerCircleVM.isRecording ? Color.orange : NirvanaColor.teal.opacity(0.5)) + .background(self.innerCircleVM.isRecording ? Color.orange : Color.teal.opacity(0.5)) .frame(width: 40, height: 40) .clipShape(Circle()) .shadow(radius: 10) @@ -104,8 +110,14 @@ struct CircleNavigationView: View { .foregroundColor(Color.green) case .offline: Circle() - default: + .frame(width: 10, height: 10) + .foregroundColor(Color.red) + case .inConvo: Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.orange) + default: + EmptyView() } } } diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift index a8b9767..a44c8ed 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift @@ -9,10 +9,10 @@ import SwiftUI import NavigationStack struct InnerCircleView: View { + @StateObject var convoViewModel: ConvoViewModel = ConvoViewModel() @StateObject var innerCircleVM: InnerCircleViewModel = InnerCircleViewModel() @EnvironmentObject var authSessionStore: AuthSessionStore - @EnvironmentObject var navigationStack: NavigationStack - + @EnvironmentObject var navigationStack: NavigationStack @State var selectedFriendIndex: String? = nil @@ -27,7 +27,7 @@ struct InnerCircleView: View { // background WavesGlassBackgroundView(isRecording: self.innerCircleVM.isRecording) - // stale states + // stale state: create profile if self.authSessionStore.user?.nickname == nil || self.authSessionStore.user?.avatar == nil { VStack(alignment: .center) { Image("undraw_fall_is_coming_yl-0-x") @@ -53,10 +53,9 @@ struct InnerCircleView: View { .font(.caption) .foregroundColor(NirvanaColor.teal) .multilineTextAlignment(.center) - } .padding() - } + } // add friend: stale state else if self.authSessionStore.friendsArr.count == 0 && self.authSessionStore.inboxUsersArr.count == 0 { VStack(alignment: .center) { Image("undraw_fall_is_coming_yl-0-x") @@ -85,10 +84,11 @@ struct InnerCircleView: View { } .padding() - } else { - // content + } else { + // inner circle grid CircleGridView(selectedFriendIndex: self.$selectedFriendIndex) - .environmentObject(innerCircleVM) + .environmentObject(self.innerCircleVM) + .environmentObject(self.convoViewModel) } // header @@ -98,7 +98,12 @@ struct InnerCircleView: View { CircleNavigationView(alertActive: self.$alertActive, alertText: self.$alertText, alertSubtext: self.$alertSubtext).environmentObject(innerCircleVM) } - CircleFooterView(selectedFriendIndex: self.$selectedFriendIndex).environmentObject(innerCircleVM) + CircleFooterView(selectedFriendIndex: self.$selectedFriendIndex) + .environmentObject(self.innerCircleVM) + .environmentObject(self.convoViewModel) + + ConvoFooterView() + .environmentObject(self.convoViewModel) // helper for new users // TODO: make it back to 1 instead of 10...testing diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift index 487994c..064ab3d 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift @@ -21,6 +21,7 @@ class InnerCircleViewModel: ObservableObject { private let cloudStorageService = CloudStorageService() private let firestoreService = FirestoreService() private let pushNotificationService = PushNotificationService() + private let agoraService = AgoraService() init() { // separate set up for listening vs recording @@ -139,3 +140,15 @@ extension InnerCircleViewModel { } } + +// everything related to calls and such +extension InnerCircleViewModel { + func getAgoraToken() { + if let uid = AuthSessionStore.getCurrentUserId() { + self.agoraService.getAgoraUserTokenServer(channelName: uid) + } + else { + print("no user authenticated to get an agora token") + } + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgoraViewController.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgoraViewController.swift new file mode 100644 index 0000000..ecfaadb --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgoraViewController.swift @@ -0,0 +1,41 @@ +// +// AgoraViewController.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import Foundation +import AgoraRtcKit + +class AgoraViewController: UIViewController { + var agoraKit: AgoraRtcEngineKit? + var agoraDelegate: AgoraRtcEngineDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + initializeAgoraEngine() + joinChannel() + } + + func initializeAgoraEngine() { + agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: agoraDelegate) + } + + func joinChannel() { + agoraKit?.joinChannel(byToken: "006c8dfd65deb5c4741bd564085627139d0IABNfXkzm8r9a6sFHwJaWORj6MMdAbbTDypvdgmCjbw2JKHYMoUAAAAAEADQ943gkE7NYQEAAQCQTs1h", channelId: "TestChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in}) + } + + func leaveChannel() { + agoraKit?.leaveChannel(nil) + } + + func destroyInstance() { + AgoraRtcEngineKit.destroy() + } + + override func viewWillDisappear(_ animated: Bool) { + leaveChannel() + destroyInstance() + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgroRep.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgroRep.swift new file mode 100644 index 0000000..b377c6f --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgroRep.swift @@ -0,0 +1,34 @@ +// +// AgroRep.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import Foundation +import SwiftUI +import AgoraRtcKit + +struct AgoraRep: UIViewControllerRepresentable { + + func makeUIViewController(context: Context) -> some UIViewController { + let agoraViewController = AgoraViewController() + agoraViewController.agoraDelegate = context.coordinator + return agoraViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + + } + + class Coordinator: NSObject, AgoraRtcEngineDelegate { + var parent: AgoraRep + init(_ agoraRep: AgoraRep) { + parent = agoraRep + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCallTest/CallView.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/CallView.swift new file mode 100644 index 0000000..7b9e863 --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCallTest/CallView.swift @@ -0,0 +1,45 @@ +// +// CallView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import SwiftUI + +struct CallView: View { + @Environment(\.presentationMode) var presentationMode + + var body: some View { + VStack { + Text("Welcome to the call!") + .bold() + Spacer() + HStack { + Image(systemName: "mic.circle.fill") + .font(.system(size:64.0)) + .foregroundColor(.blue) + .padding() + Spacer() + Image(systemName: "phone.circle.fill") + .font(.system(size:64.0)) + .foregroundColor(.red) + .padding() + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + } + + AgoraRep() + .frame(width: 0, height: 0, alignment: .center) + + .padding() + } + } +} + +struct CallView_Previews: PreviewProvider { + static var previews: some View { + CallView() + } +} diff --git a/nirvana-ios/Views/Router/RouterView.swift b/nirvana-ios/Views/Router/RouterView.swift index 6c25859..fd56c69 100644 --- a/nirvana-ios/Views/Router/RouterView.swift +++ b/nirvana-ios/Views/Router/RouterView.swift @@ -19,36 +19,16 @@ enum NavigationPages { struct RouterView: View { @EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var navigationStack: NavigationStack - @Environment(\.scenePhase) var scenePhase var body: some View { - ZStack { - switch self.authSessionStore.sessionState { - case SessionState.notCheckedYet: - SplashView() - case SessionState.isAuthenticated: - InnerCircleView() - case SessionState.isLoggedOut: - WelcomeView() - } - } - .onChange(of: scenePhase) { newPhase in - if newPhase == .inactive { - print("user is offline") - // set firestore user document isOnline to false - self.authSessionStore.updateUserStatus(userStatus: .offline) - } else if newPhase == .active { - print("user is online again") - // set firestore user document isOnline to true - self.authSessionStore.updateUserStatus(userStatus: .online) - } else if newPhase == .background { - // TODO: find a way to do this in the background so that people can call me and start talking even if it's in the background - // but this may just not be possible, only with an actual call so to speak - print("app is in backgroun") - self.authSessionStore.updateUserStatus(userStatus: .background) - } - } - + switch self.authSessionStore.sessionState { + case SessionState.notCheckedYet: + SplashView() + case SessionState.isAuthenticated: + InnerCircleView() + case SessionState.isLoggedOut: + WelcomeView() + } } } diff --git a/nirvana-ios/nirvana_iosApp.swift b/nirvana-ios/nirvana_iosApp.swift index 2b63f8f..9bc202b 100644 --- a/nirvana-ios/nirvana_iosApp.swift +++ b/nirvana-ios/nirvana_iosApp.swift @@ -13,6 +13,7 @@ import FirebaseMessaging @main struct nirvana_iosApp: App { @StateObject var authSessionStore: AuthSessionStore = AuthSessionStore() + @Environment(\.scenePhase) var scenePhase @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate var body: some Scene { @@ -24,6 +25,25 @@ struct nirvana_iosApp: App { Auth.auth().canHandle(url) // <- just for information purposes } } + .onChange(of: scenePhase) { newPhase in + switch newPhase { + case .inactive: + print("user is offline") + // set firestore user document isOnline to false + self.authSessionStore.updateUserStatus(userStatus: .offline) + case .active: + print("user is online again") + // set firestore user document isOnline to true + self.authSessionStore.updateUserStatus(userStatus: .online) + case .background: + // TODO: find a way to do this in the background so that people can call me and start talking even if it's in the background + // but this may just not be possible, only with an actual call so to speak + print("app is in backgroun") + self.authSessionStore.updateUserStatus(userStatus: .background) + default: + print("default state of app") + } + } } } diff --git a/nirvana-ios/sounds/joinSound.mp3 b/nirvana-ios/sounds/joinSound.mp3 new file mode 100644 index 0000000..4a42d9b Binary files /dev/null and b/nirvana-ios/sounds/joinSound.mp3 differ diff --git a/nirvana-ios/sounds/leaveSound.mp3 b/nirvana-ios/sounds/leaveSound.mp3 new file mode 100644 index 0000000..ef17270 Binary files /dev/null and b/nirvana-ios/sounds/leaveSound.mp3 differ