Merge pull request #3 from talksik/live-voice-call-api-integration

Live voice call api integration
This commit is contained in:
Arjun Patel
2021-12-31 03:15:33 -08:00
committed by GitHub
32 changed files with 1443 additions and 59 deletions
+85
View File
@@ -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>
@@ -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
}
}
@@ -0,0 +1,12 @@
{
"data" : [
{
"filename" : "leaveSound.mp3",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+40
View File
@@ -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() {
//
// }
//}
+2
View File
@@ -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)))
}
}
}
+75
View File
@@ -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)
}
}
}
}
+3 -3
View File
@@ -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
}
}
+76
View File
@@ -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()
}
}
+8 -28
View File
@@ -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()
}
}
}
+20
View File
@@ -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.