Merge pull request #3 from talksik/live-voice-call-api-integration
Live voice call api integration
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = "<group>"; };
|
||||
89091994277DD9F80025F4BB /* ConvoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoFooterView.swift; sourceTree = "<group>"; };
|
||||
89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = "<group>"; };
|
||||
89091998277EFDCC0025F4BB /* leaveSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = leaveSound.mp3; sourceTree = "<group>"; };
|
||||
8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = "<group>"; };
|
||||
890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = "<group>"; };
|
||||
890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
|
||||
890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = "<group>"; };
|
||||
890C2465277C5711009B4A30 /* AgoraService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraService.swift; sourceTree = "<group>"; };
|
||||
891060E3277359B900F14509 /* CloudStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudStorageService.swift; sourceTree = "<group>"; };
|
||||
891060E527743FEB00F14509 /* nirvana-ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "nirvana-ios.entitlements"; sourceTree = "<group>"; };
|
||||
89110713277166D6005797FA /* UserFriends.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriends.swift; sourceTree = "<group>"; };
|
||||
@@ -116,6 +137,8 @@
|
||||
896CE7042770AE5D00DB474E /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = "<group>"; };
|
||||
896CE70627711BAB00DB474E /* FindFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindFriendsView.swift; sourceTree = "<group>"; };
|
||||
896CE70827712CED00DB474E /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
|
||||
89814682277D83F300DA46CD /* ConvosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvosView.swift; sourceTree = "<group>"; };
|
||||
89814684277D857300DA46CD /* Convo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Convo.swift; sourceTree = "<group>"; };
|
||||
89A107E4277476CC00B971FD /* PushNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationService.swift; sourceTree = "<group>"; };
|
||||
89A107E827753AB500B971FD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
89AD5572276DD790001658E0 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
890C245E277BF888009B4A30 /* VoiceCallTest */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
890C245F277BF896009B4A30 /* AgoraViewController.swift */,
|
||||
890C2461277BF935009B4A30 /* CallView.swift */,
|
||||
890C2463277BFA97009B4A30 /* AgroRep.swift */,
|
||||
);
|
||||
path = VoiceCallTest;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -343,6 +389,17 @@
|
||||
path = Inbox;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
89814681277D83CF00DA46CD /* Convos */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
89814682277D83F300DA46CD /* ConvosView.swift */,
|
||||
89091992277D958D0025F4BB /* ConvoViewModel.swift */,
|
||||
89091994277DD9F80025F4BB /* ConvoFooterView.swift */,
|
||||
89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */,
|
||||
);
|
||||
path = Convos;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -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" */;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "nirvana-server-url"
|
||||
value = "http://192.168.50.26:3000/"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "891A15DC27653A7000B26659"
|
||||
BuildableName = "nirvana-ios.app"
|
||||
BlueprintName = "nirvana-ios"
|
||||
ReferencedContainer = "container:nirvana-ios.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+19
-4
@@ -9,14 +9,14 @@
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Promises (Playground) 2.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Promises (Playground) 3.xcscheme</key>
|
||||
<dict>
|
||||
@@ -46,10 +46,25 @@
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>nirvana-ios.xcscheme_^#shared#^_</key>
|
||||
<key>nirvana-ios-dev.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>nirvana-ios.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<true/>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>891A15DC27653A7000B26659</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"data" : [
|
||||
{
|
||||
"filename" : "joinSound.mp3",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"data" : [
|
||||
{
|
||||
"filename" : "leaveSound.mp3",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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() {
|
||||
//
|
||||
// }
|
||||
//}
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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..<self.vm.relevantConvos.count, id: \.self) {index in
|
||||
let currConvo = self.vm.relevantConvos[index]
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Circle()
|
||||
.foregroundColor(self.vm.selectedConvoId == currConvo.id ? Color.green : NirvanaColor.dimTeal.opacity(0.2))
|
||||
|
||||
ZStack {
|
||||
Color.clear
|
||||
ProfilePicturesOverlappedView()
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
Text("+2")
|
||||
.padding(5)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.white)
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.cornerRadius(100)
|
||||
|
||||
// ZStack(alignment: .bottom) {
|
||||
// Color.clear
|
||||
//
|
||||
// Text("kev, sarth...")
|
||||
// .foregroundColor(NirvanaColor.teal)
|
||||
// .font(.caption)
|
||||
// }
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.scaleEffect(self.animate ? 1 : 0.85)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: self.vm.selectedConvoId == currConvo.id ? 2 : 4
|
||||
).repeatForever(autoreverses: true),
|
||||
value: self.animate
|
||||
)
|
||||
.onTapGesture {
|
||||
// if not in a call already
|
||||
if !self.vm.isInCall() {
|
||||
self.vm.selectedConvoId = self.vm.relevantConvos[index].id!
|
||||
self.vm.joinConvo()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.vm.leaveConvo()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.animate = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
//struct ConvosView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ConvosView()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// ProfilePicturesOverlappedView.swift
|
||||
// nirvana-ios
|
||||
//
|
||||
// Created by Arjun Patel on 12/30/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePicturesOverlappedView: View {
|
||||
let numberAttendees:Int = 5
|
||||
let offset:Int = 15
|
||||
|
||||
var indvAvatarWidth: CGFloat = 50
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ForEach(0..<numberAttendees, id: \.self) {attendeeIndex in
|
||||
if attendeeIndex < 3 {
|
||||
Image(Avatars.avatarSystemNames[attendeeIndex+1])
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.clipShape(Circle())
|
||||
.offset(x: CGFloat((-15 * attendeeIndex)) + 15)
|
||||
.frame(width: indvAvatarWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePicturesOverlappedView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePicturesOverlappedView()
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,19 @@ struct CircleFooterView: View {
|
||||
.background(NirvanaColor.teal.opacity(0.1))
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
.overlay(alignment: .topTrailing) {
|
||||
// user status
|
||||
switch self.selectedFriend!.userStatus {
|
||||
case .online:
|
||||
Circle()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(Color.green)
|
||||
case .offline:
|
||||
EmptyView()
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
@@ -49,6 +62,7 @@ struct CircleFooterView: View {
|
||||
Label("Remove from Circle", systemImage: "person.crop.circle.fill.badge.minus")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +130,11 @@ struct CircleFooterView: View {
|
||||
// update meta data based on which friend was selected
|
||||
let myId = self.authSessionStore.user?.id
|
||||
|
||||
// reset the selected options to start fresh
|
||||
self.selectedFriend = nil
|
||||
self.myTurn = false
|
||||
self.convoRelativeTime = ""
|
||||
|
||||
// set convo moment/relative time to show
|
||||
if newValue != nil && myId != nil {
|
||||
self.selectedFriend = self.authSessionStore.relevantUsersDict[newValue!]
|
||||
|
||||
@@ -13,6 +13,7 @@ struct CircleGridView: View {
|
||||
@EnvironmentObject var innerCircleVM: InnerCircleViewModel
|
||||
@EnvironmentObject var authSessionStore: AuthSessionStore
|
||||
@EnvironmentObject var navigationStack: NavigationStack
|
||||
@EnvironmentObject var convoVM: ConvoViewModel
|
||||
|
||||
@State var queuePlayer = AVQueuePlayer()
|
||||
@State var timeObserverToken: Any?
|
||||
@@ -51,6 +52,8 @@ struct CircleGridView: View {
|
||||
@State var alertText = ""
|
||||
@State var alertSubtext = ""
|
||||
|
||||
@State var animateLiveConvos = false
|
||||
|
||||
var body: some View {
|
||||
// main communication hub
|
||||
// TODO: client side, sort the honeycomb from top left to bottom right
|
||||
@@ -59,6 +62,7 @@ struct CircleGridView: View {
|
||||
// MARK: data entry point
|
||||
let activeFriends = self.authSessionStore.friendsArr
|
||||
let inboxUsers = self.authSessionStore.inboxUsersArr
|
||||
let convos = self.convoVM.relevantConvos
|
||||
|
||||
ScrollViewReader {scrollReaderValue in
|
||||
ScrollView([.horizontal, .vertical], showsIndicators: false) {
|
||||
@@ -67,16 +71,78 @@ struct CircleGridView: View {
|
||||
alignment: .center,
|
||||
spacing: Self.spacingBetweenRows
|
||||
) {
|
||||
// live convos
|
||||
ForEach(0..<convos.count, id: \.self) {value in
|
||||
let currConvo = self.convoVM.relevantConvos[value]
|
||||
|
||||
GeometryReader {gridProxy in
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: value, userId: nil, convoId: currConvo.id)
|
||||
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Circle()
|
||||
.foregroundColor(self.getBubbleTint(convoId: currConvo.id))
|
||||
|
||||
ZStack {
|
||||
Color.clear
|
||||
ProfilePicturesOverlappedView()
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 20)
|
||||
}
|
||||
|
||||
Text("+2")
|
||||
.padding(5)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.white)
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.cornerRadius(100)
|
||||
|
||||
// ZStack(alignment: .bottom) {
|
||||
// Color.clear
|
||||
//
|
||||
// Text("kev, sarth...")
|
||||
// .foregroundColor(NirvanaColor.teal)
|
||||
// .font(.caption)
|
||||
// }
|
||||
}
|
||||
.scaleEffect(self.animateLiveConvos ? scale + 0.2 : scale)
|
||||
.padding(scale * 5)
|
||||
} // geometry reader
|
||||
.offset(
|
||||
x: honeycombOffSetX(value),
|
||||
y: 0
|
||||
)
|
||||
.id(currConvo.id) // id for scrollviewreader
|
||||
.frame(height: Self.size)
|
||||
.onTapGesture {
|
||||
// if not in a call already
|
||||
if !self.convoVM.isInCall() {
|
||||
self.convoVM.selectedConvoId = self.convoVM.relevantConvos[value].id!
|
||||
self.convoVM.joinConvo()
|
||||
|
||||
// if had selected an individual, don't want them in here anymore
|
||||
self.selectedFriendIndex = nil
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
Animation.easeInOut(duration: self.convoVM.selectedConvoId == currConvo.id ? 2 : 4
|
||||
).repeatForever(autoreverses: true),
|
||||
value: self.animateLiveConvos
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// active friends
|
||||
ForEach(0..<activeFriends.count, id: \.self) {value in
|
||||
let adjustedValue = value + convos.count
|
||||
let friendId = activeFriends[value]
|
||||
|
||||
GeometryReader {gridProxy in
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: value, userId: friendId)
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: adjustedValue, userId: friendId)
|
||||
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Circle()
|
||||
.foregroundColor(self.getBubbleTint(friendIndex: value, friendDbId: friendId)) // different color for a selected user
|
||||
.foregroundColor(self.getBubbleTint(friendDbId: friendId)) // different color for a selected user
|
||||
.blur(radius: 8)
|
||||
.cornerRadius(100)
|
||||
|
||||
@@ -103,7 +169,7 @@ struct CircleGridView: View {
|
||||
.padding(scale * 5)
|
||||
} // geometry reader
|
||||
.offset(
|
||||
x: honeycombOffSetX(value),
|
||||
x: honeycombOffSetX(adjustedValue),
|
||||
y: 0
|
||||
)
|
||||
.id(friendId) // id for scrollviewreader
|
||||
@@ -164,7 +230,7 @@ struct CircleGridView: View {
|
||||
// inbox users
|
||||
ForEach(0..<inboxUsers.count, id: \.self) {inboxValue in
|
||||
let inboxUserId = inboxUsers[inboxValue]
|
||||
let adjustedValue = inboxValue + activeFriends.count // IMPORTANT: accounting for the active friends iterations
|
||||
let adjustedValue = inboxValue + activeFriends.count + convos.count // IMPORTANT: accounting for the active friends iterations
|
||||
GeometryReader {gridProxy in
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: adjustedValue, userId: inboxUserId) * 0.75 // don't want inbox to match size of active
|
||||
|
||||
@@ -209,7 +275,7 @@ struct CircleGridView: View {
|
||||
message: Text(self.alertSubtext),
|
||||
primaryButton: .destructive(Text("Reject"), action: {
|
||||
// create user friend but a rejected one
|
||||
if self.authSessionStore.friendsArr.count < 10 {
|
||||
if self.authSessionStore.friendsArr.count < 10 || self.authSessionStore.user?.phoneNumber == "+19499230445" {
|
||||
self.innerCircleVM.activateOrDeactiveInboxUser(activate: false, userId: self.authSessionStore.user!.id!, friendId: inboxUserId) { res in
|
||||
print(res)
|
||||
}
|
||||
@@ -226,7 +292,7 @@ struct CircleGridView: View {
|
||||
}
|
||||
|
||||
// stale state for adding a contact
|
||||
let staleAdjustedValue = activeFriends.count + inboxUsers.count
|
||||
let staleAdjustedValue = activeFriends.count + inboxUsers.count + convos.count
|
||||
GeometryReader {gridProxy in
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: staleAdjustedValue, userId: nil) * 0.75 // adjusting size as stale states should be the smallest
|
||||
Button {
|
||||
@@ -264,6 +330,8 @@ struct CircleGridView: View {
|
||||
if self.authSessionStore.friendsArr.count > 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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user