From 04affa99fe693846b403abcc61c07939ad11061e Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 28 Dec 2021 17:59:16 -0800 Subject: [PATCH 01/27] adding package --- nirvana-ios.xcodeproj/project.pbxproj | 17 +++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index f7d1a12..e900a18 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */; }; + 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */ = {isa = PBXBuildFile; productRef = 890C245C277BF628009B4A30 /* AgoraRtcKit */; }; 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 */; }; @@ -148,6 +149,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */, 89BCABE2276B3CB0009E9B10 /* NavigationStack in Frameworks */, 89F18AD4276E881800115B1E /* FirebaseAuthCombine-Community in Frameworks */, 89F18AEA276E881900115B1E /* FirebaseStorageSwift-Beta in Frameworks */, @@ -490,6 +492,7 @@ 891060E72774460500F14509 /* FirebaseInAppMessaging-Beta */, 891060E92774460500F14509 /* FirebaseInAppMessagingSwift-Beta */, 891060EB2774460500F14509 /* FirebaseMessaging */, + 890C245C277BF628009B4A30 /* AgoraRtcKit */, ); productName = "nirvana-ios"; productReference = 891A15DD27653A7000B26659 /* nirvana-ios.app */; @@ -523,6 +526,7 @@ 89F1386E2767F55F006680ED /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, 89BCABE0276B3CB0009E9B10 /* XCRemoteSwiftPackageReference "swiftui-navigation-stack" */, 89F18ACC276E881800115B1E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 890C245B277BF628009B4A30 /* XCRemoteSwiftPackageReference "AgoraAudio_iOS" */, ); productRefGroup = 891A15DE27653A7000B26659 /* Products */; projectDirPath = ""; @@ -822,6 +826,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 +861,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 890C245C277BF628009B4A30 /* AgoraRtcKit */ = { + isa = XCSwiftPackageProductDependency; + package = 890C245B277BF628009B4A30 /* XCRemoteSwiftPackageReference "AgoraAudio_iOS" */; + productName = AgoraRtcKit; + }; 891060E72774460500F14509 /* FirebaseInAppMessaging-Beta */ = { isa = XCSwiftPackageProductDependency; package = 89F18ACC276E881800115B1E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5bb82bd..f7e591c 100644 --- a/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/nirvana-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": "0.20200225.4" } }, + { + "package": "AgoraRtcKit", + "repositoryURL": "https://github.com/AgoraIO/AgoraAudio_iOS", + "state": { + "branch": null, + "revision": "74856715c8d8b381b0dbd02ddc86fbbfd0f0f6c4", + "version": "3.6.0" + } + }, { "package": "AppAuth", "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", From 4db618c7d00452f06dc3e3acbdedaf0f5aeaa11c Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 28 Dec 2021 23:19:17 -0800 Subject: [PATCH 02/27] fixing bug with updating of user status --- nirvana-ios.xcodeproj/project.pbxproj | 20 +++++++++ .../Views/InnerCircle/CircleGridView.swift | 14 +++++- .../Views/InnerCircle/InnerCircleView.swift | 3 +- .../VoiceCall/AgoraViewController.swift | 41 +++++++++++++++++ .../Views/InnerCircle/VoiceCall/AgroRep.swift | 34 ++++++++++++++ .../InnerCircle/VoiceCall/CallView.swift | 45 +++++++++++++++++++ nirvana-ios/Views/Router/RouterView.swift | 36 ++++----------- nirvana-ios/nirvana_iosApp.swift | 20 +++++++++ 8 files changed, 182 insertions(+), 31 deletions(-) create mode 100644 nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift create mode 100644 nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift create mode 100644 nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index e900a18..2e27122 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */; }; 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 */; }; 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 */; }; @@ -86,6 +89,9 @@ /* Begin PBXFileReference section */ 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingOneView.swift; sourceTree = ""; }; + 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; + 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; + 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; 891060E3277359B900F14509 /* CloudStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudStorageService.swift; sourceTree = ""; }; 891060E527743FEB00F14509 /* nirvana-ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "nirvana-ios.entitlements"; sourceTree = ""; }; 89110713277166D6005797FA /* UserFriends.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriends.swift; sourceTree = ""; }; @@ -176,6 +182,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 890C245E277BF888009B4A30 /* VoiceCall */ = { + isa = PBXGroup; + children = ( + 890C245F277BF896009B4A30 /* AgoraViewController.swift */, + 890C2461277BF935009B4A30 /* CallView.swift */, + 890C2463277BFA97009B4A30 /* AgroRep.swift */, + ); + path = VoiceCall; + sourceTree = ""; + }; 891060E62774460500F14509 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -374,6 +390,7 @@ 89BCABDF276B3588009E9B10 /* InnerCircle */ = { isa = PBXGroup; children = ( + 890C245E277BF888009B4A30 /* VoiceCall */, 89BCABE4276B3D58009E9B10 /* InnerCircleView.swift */, 89BCABE6276B3D64009E9B10 /* InnerCircleViewModel.swift */, 896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */, @@ -560,6 +577,7 @@ 891060E4277359B900F14509 /* CloudStorageService.swift in Sources */, 896CE7002770847400DB474E /* CircleFooterView.swift in Sources */, 894E80BE276ED7A700624B72 /* avatars.swift in Sources */, + 890C2462277BF935009B4A30 /* CallView.swift in Sources */, 896CE7052770AE5D00DB474E /* InboxView.swift in Sources */, 891A160327656FC700B26659 /* HomeView.swift in Sources */, 896CE70927712CED00DB474E /* StringExtension.swift in Sources */, @@ -578,6 +596,7 @@ 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 */, @@ -603,6 +622,7 @@ 89F18AF5276ECF2A00115B1E /* AddBasicInfoView.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 */, diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 12de40b..18c1c9c 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -17,6 +17,8 @@ struct CircleGridView: View { @State var queuePlayer = AVQueuePlayer() @State var timeObserverToken: Any? + @State private var showCall: Bool = false + @GestureState var dragState = DragState.inactive let universalSize = UIScreen.main.bounds @@ -209,7 +211,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) } @@ -266,6 +268,9 @@ struct CircleGridView: View { } } } // scrollview reader + .fullScreenCover(isPresented: $showCall, content: { + CallView() + }) } @@ -379,6 +384,13 @@ extension CircleGridView { 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 + // TESTING CALL + let testingCall = false + if testingCall { + self.showCall.toggle() + return + } + print("tap gesture activated") // clearing the player to make room for this friend's convo or to deselect this user diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift index a8b9767..fb3d98d 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift @@ -11,8 +11,7 @@ import NavigationStack struct InnerCircleView: View { @StateObject var innerCircleVM: InnerCircleViewModel = InnerCircleViewModel() @EnvironmentObject var authSessionStore: AuthSessionStore - @EnvironmentObject var navigationStack: NavigationStack - + @EnvironmentObject var navigationStack: NavigationStack @State var selectedFriendIndex: String? = nil diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift b/nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift new file mode 100644 index 0000000..ecfaadb --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift @@ -0,0 +1,41 @@ +// +// AgoraViewController.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import Foundation +import AgoraRtcKit + +class AgoraViewController: UIViewController { + var agoraKit: AgoraRtcEngineKit? + var agoraDelegate: AgoraRtcEngineDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + initializeAgoraEngine() + joinChannel() + } + + func initializeAgoraEngine() { + agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: agoraDelegate) + } + + func joinChannel() { + agoraKit?.joinChannel(byToken: "006c8dfd65deb5c4741bd564085627139d0IABNfXkzm8r9a6sFHwJaWORj6MMdAbbTDypvdgmCjbw2JKHYMoUAAAAAEADQ943gkE7NYQEAAQCQTs1h", channelId: "TestChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in}) + } + + func leaveChannel() { + agoraKit?.leaveChannel(nil) + } + + func destroyInstance() { + AgoraRtcEngineKit.destroy() + } + + override func viewWillDisappear(_ animated: Bool) { + leaveChannel() + destroyInstance() + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift b/nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift new file mode 100644 index 0000000..b377c6f --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift @@ -0,0 +1,34 @@ +// +// AgroRep.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import Foundation +import SwiftUI +import AgoraRtcKit + +struct AgoraRep: UIViewControllerRepresentable { + + func makeUIViewController(context: Context) -> some UIViewController { + let agoraViewController = AgoraViewController() + agoraViewController.agoraDelegate = context.coordinator + return agoraViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + + } + + class Coordinator: NSObject, AgoraRtcEngineDelegate { + var parent: AgoraRep + init(_ agoraRep: AgoraRep) { + parent = agoraRep + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift b/nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift new file mode 100644 index 0000000..7b9e863 --- /dev/null +++ b/nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift @@ -0,0 +1,45 @@ +// +// CallView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/28/21. +// + +import SwiftUI + +struct CallView: View { + @Environment(\.presentationMode) var presentationMode + + var body: some View { + VStack { + Text("Welcome to the call!") + .bold() + Spacer() + HStack { + Image(systemName: "mic.circle.fill") + .font(.system(size:64.0)) + .foregroundColor(.blue) + .padding() + Spacer() + Image(systemName: "phone.circle.fill") + .font(.system(size:64.0)) + .foregroundColor(.red) + .padding() + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + } + + AgoraRep() + .frame(width: 0, height: 0, alignment: .center) + + .padding() + } + } +} + +struct CallView_Previews: PreviewProvider { + static var previews: some View { + CallView() + } +} diff --git a/nirvana-ios/Views/Router/RouterView.swift b/nirvana-ios/Views/Router/RouterView.swift index 6c25859..fd56c69 100644 --- a/nirvana-ios/Views/Router/RouterView.swift +++ b/nirvana-ios/Views/Router/RouterView.swift @@ -19,36 +19,16 @@ enum NavigationPages { struct RouterView: View { @EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var navigationStack: NavigationStack - @Environment(\.scenePhase) var scenePhase var body: some View { - ZStack { - switch self.authSessionStore.sessionState { - case SessionState.notCheckedYet: - SplashView() - case SessionState.isAuthenticated: - InnerCircleView() - case SessionState.isLoggedOut: - WelcomeView() - } - } - .onChange(of: scenePhase) { newPhase in - if newPhase == .inactive { - print("user is offline") - // set firestore user document isOnline to false - self.authSessionStore.updateUserStatus(userStatus: .offline) - } else if newPhase == .active { - print("user is online again") - // set firestore user document isOnline to true - self.authSessionStore.updateUserStatus(userStatus: .online) - } else if newPhase == .background { - // TODO: find a way to do this in the background so that people can call me and start talking even if it's in the background - // but this may just not be possible, only with an actual call so to speak - print("app is in backgroun") - self.authSessionStore.updateUserStatus(userStatus: .background) - } - } - + switch self.authSessionStore.sessionState { + case SessionState.notCheckedYet: + SplashView() + case SessionState.isAuthenticated: + InnerCircleView() + case SessionState.isLoggedOut: + WelcomeView() + } } } diff --git a/nirvana-ios/nirvana_iosApp.swift b/nirvana-ios/nirvana_iosApp.swift index 2b63f8f..9bc202b 100644 --- a/nirvana-ios/nirvana_iosApp.swift +++ b/nirvana-ios/nirvana_iosApp.swift @@ -13,6 +13,7 @@ import FirebaseMessaging @main struct nirvana_iosApp: App { @StateObject var authSessionStore: AuthSessionStore = AuthSessionStore() + @Environment(\.scenePhase) var scenePhase @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate var body: some Scene { @@ -24,6 +25,25 @@ struct nirvana_iosApp: App { Auth.auth().canHandle(url) // <- just for information purposes } } + .onChange(of: scenePhase) { newPhase in + switch newPhase { + case .inactive: + print("user is offline") + // set firestore user document isOnline to false + self.authSessionStore.updateUserStatus(userStatus: .offline) + case .active: + print("user is online again") + // set firestore user document isOnline to true + self.authSessionStore.updateUserStatus(userStatus: .online) + case .background: + // TODO: find a way to do this in the background so that people can call me and start talking even if it's in the background + // but this may just not be possible, only with an actual call so to speak + print("app is in backgroun") + self.authSessionStore.updateUserStatus(userStatus: .background) + default: + print("default state of app") + } + } } } From 9bc6462f344fcbec67e1abd46928aefb3b8e41f8 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 00:19:57 -0800 Subject: [PATCH 03/27] adding another scheme to allow connection to unique server url for node server --- .../xcschemes/nirvana-ios-dev.xcscheme | 85 +++++++++++++++++++ .../xcschemes/nirvana-ios.xcscheme | 78 +++++++++++++++++ .../xcschemes/xcschememanagement.plist | 23 ++++- 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme create mode 100644 nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme diff --git a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme new file mode 100644 index 0000000..6753332 --- /dev/null +++ b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme new file mode 100644 index 0000000..e692ea2 --- /dev/null +++ b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist b/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist index a6053c8..72de430 100644 --- a/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/nirvana-ios.xcodeproj/xcuserdata/talksik.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,14 +9,14 @@ isShown orderHint - 3 + 2 Promises (Playground) 2.xcscheme isShown orderHint - 4 + 3 Promises (Playground) 3.xcscheme @@ -46,10 +46,25 @@ orderHint 0 - nirvana-ios.xcscheme_^#shared#^_ + nirvana-ios-dev.xcscheme_^#shared#^_ orderHint - 0 + 4 + + nirvana-ios.xcscheme_^#shared#^_ + + isShown + + orderHint + 1 + + + SuppressBuildableAutocreation + + 891A15DC27653A7000B26659 + + primary + From ba383dfdb0cb9d917aa88765e7513f11c98e5052 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 01:53:29 -0800 Subject: [PATCH 04/27] getting basic call to backend for a token but it's messy as hellll!!!! --- nirvana-ios.xcodeproj/project.pbxproj | 4 ++ .../xcschemes/nirvana-ios-dev.xcscheme | 2 +- nirvana-ios/Services/AgoraService.swift | 47 +++++++++++++++++++ nirvana-ios/Services/AuthSessionStore.swift | 6 +-- .../Views/InnerCircle/CircleGridView.swift | 3 ++ .../InnerCircle/InnerCircleViewModel.swift | 12 +++++ 6 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 nirvana-ios/Services/AgoraService.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 2e27122..87bbc6d 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 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 */; }; @@ -92,6 +93,7 @@ 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; + 890C2465277C5711009B4A30 /* AgoraService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraService.swift; sourceTree = ""; }; 891060E3277359B900F14509 /* CloudStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudStorageService.swift; sourceTree = ""; }; 891060E527743FEB00F14509 /* nirvana-ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "nirvana-ios.entitlements"; sourceTree = ""; }; 89110713277166D6005797FA /* UserFriends.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriends.swift; sourceTree = ""; }; @@ -435,6 +437,7 @@ 89F05BA3276ACC060002E9C7 /* ContactService.swift */, 89B46E70276893E200B74255 /* AuthSessionStore.swift */, 89A107E4277476CC00B971FD /* PushNotificationService.swift */, + 890C2465277C5711009B4A30 /* AgoraService.swift */, ); path = Services; sourceTree = ""; @@ -601,6 +604,7 @@ 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 */, diff --git a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme index 6753332..911ea35 100644 --- a/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme +++ b/nirvana-ios.xcodeproj/xcshareddata/xcschemes/nirvana-ios-dev.xcscheme @@ -53,7 +53,7 @@ diff --git a/nirvana-ios/Services/AgoraService.swift b/nirvana-ios/Services/AgoraService.swift new file mode 100644 index 0000000..ed0e723 --- /dev/null +++ b/nirvana-ios/Services/AgoraService.swift @@ -0,0 +1,47 @@ +// +// AgoraService.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import FirebaseAuth +import Firebase + +class AgoraService { + func getAgoraUserToken(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") + + 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() + } + } +} diff --git a/nirvana-ios/Services/AuthSessionStore.swift b/nirvana-ios/Services/AuthSessionStore.swift index f60989a..cbc2740 100644 --- a/nirvana-ios/Services/AuthSessionStore.swift +++ b/nirvana-ios/Services/AuthSessionStore.swift @@ -140,7 +140,7 @@ final class AuthSessionStore: ObservableObject, SessionStore { } } - func getCurrentUserId() -> String? { + static func getCurrentUserId() -> String? { return Auth.auth().currentUser?.uid } @@ -214,7 +214,7 @@ extension AuthSessionStore { } // can't get proper listeners working if don't have current user's id - let currUserId = self.getCurrentUserId() + let currUserId = AuthSessionStore.getCurrentUserId() if currUserId == nil { print("no user id available to initiate data listeners") return @@ -457,7 +457,7 @@ extension AuthSessionStore { print("user already has status: \(userStatus)") } else { - if let uid = self.getCurrentUserId() { + if let uid = AuthSessionStore.getCurrentUserId() { self.firestoreService.updateUserStatus(userId: uid, userStatus: userStatus) {res in print(res) } diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 18c1c9c..88bb68b 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -266,6 +266,9 @@ struct CircleGridView: View { if self.authSessionStore.friendsArr.count > 0 { scrollReaderValue.scrollTo(self.selectedFriendIndex) } + + // get this user's agora token + self.innerCircleVM.getAgoraToken() } } // scrollview reader .fullScreenCover(isPresented: $showCall, content: { diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift index 487994c..cc8f18a 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleViewModel.swift @@ -21,6 +21,7 @@ class InnerCircleViewModel: ObservableObject { private let cloudStorageService = CloudStorageService() private let firestoreService = FirestoreService() private let pushNotificationService = PushNotificationService() + private let agoraService = AgoraService() init() { // separate set up for listening vs recording @@ -139,3 +140,14 @@ extension InnerCircleViewModel { } } + +extension InnerCircleViewModel { + func getAgoraToken() { + if let uid = AuthSessionStore.getCurrentUserId() { + self.agoraService.getAgoraUserToken(channelName: uid) + } + else { + print("no user authenticated to get an agora token") + } + } +} From dccba8389adee531bb6417d380240161d70cfd56 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 17:06:44 -0800 Subject: [PATCH 05/27] saving last updated status in db --- nirvana-ios/Repositories/FirestoreService.swift | 2 +- nirvana-ios/Services/AgoraService.swift | 1 + nirvana-ios/Views/InnerCircle/CircleGridView.swift | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/nirvana-ios/Repositories/FirestoreService.swift b/nirvana-ios/Repositories/FirestoreService.swift index 9a7b480..0b6be0f 100644 --- a/nirvana-ios/Repositories/FirestoreService.swift +++ b/nirvana-ios/Repositories/FirestoreService.swift @@ -175,7 +175,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)") diff --git a/nirvana-ios/Services/AgoraService.swift b/nirvana-ios/Services/AgoraService.swift index ed0e723..026b213 100644 --- a/nirvana-ios/Services/AgoraService.swift +++ b/nirvana-ios/Services/AgoraService.swift @@ -26,6 +26,7 @@ class AgoraService { 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 { diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 88bb68b..db31333 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -265,10 +265,7 @@ struct CircleGridView: View { // scrolling to first person in grid if self.authSessionStore.friendsArr.count > 0 { scrollReaderValue.scrollTo(self.selectedFriendIndex) - } - - // get this user's agora token - self.innerCircleVM.getAgoraToken() + } } } // scrollview reader .fullScreenCover(isPresented: $showCall, content: { From 54c5cf6e206c6405090b9bec6d37924fe0ad88ee Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 18:24:16 -0800 Subject: [PATCH 06/27] adding stuff --- nirvana-ios/Models/User.swift | 1 + nirvana-ios/Views/Contacts/FindFriendsView.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nirvana-ios/Models/User.swift b/nirvana-ios/Models/User.swift index c824f3a..fe508a3 100644 --- a/nirvana-ios/Models/User.swift +++ b/nirvana-ios/Models/User.swift @@ -26,6 +26,7 @@ struct User: Identifiable, Codable { var userStatus: UserStatus? + var lastUpdatedStatusTimestamp: Date? @ServerTimestamp var lastLoggedInTimestamp: Date? @ServerTimestamp var createdTimestamp: Date? diff --git a/nirvana-ios/Views/Contacts/FindFriendsView.swift b/nirvana-ios/Views/Contacts/FindFriendsView.swift index bf992f9..44e7d7d 100644 --- a/nirvana-ios/Views/Contacts/FindFriendsView.swift +++ b/nirvana-ios/Views/Contacts/FindFriendsView.swift @@ -135,7 +135,7 @@ struct ListContactRow: View { secondaryButton: .default(Text("Confirm")) { print("adding contact to circle") // call method in vm to get it done, then navigate to the circle - if self.authSessionStore.friendsArr.count < 10 { + if self.authSessionStore.friendsArr.count < 10 || self.authSessionStore.user?.phoneNumber == "+19499230445" { self.contactsVM.addOrActivateFriendToCircle(userId: self.authSessionStore.user!.id!, friendId: (contact.user!.id)!) {res in print(res) From 5a5e49b0a1524ff77bbadb3181c7ce124e2dc8b8 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 18:36:59 -0800 Subject: [PATCH 07/27] adjustment to status in footer --- .../Views/InnerCircle/CircleFooterView.swift | 14 ++++++++++++++ .../Views/InnerCircle/CircleNavigationView.swift | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index caf4f0f..1d28232 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -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: + Circle() + default: + Circle() + } + } .padding(5) .contextMenu { Button(role: .destructive) { @@ -49,6 +62,7 @@ struct CircleFooterView: View { Label("Remove from Circle", systemImage: "person.crop.circle.fill.badge.minus") } } + } diff --git a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift index 0255d52..6e57e04 100644 --- a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift @@ -91,7 +91,7 @@ struct CircleNavigationView: View { 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) From c559a25f29e328ec76892355f9937e72c7c0b242 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 22:05:34 -0800 Subject: [PATCH 08/27] fixing circles for status showing black and covering. --- nirvana-ios/Views/InnerCircle/CircleFooterView.swift | 4 ++-- nirvana-ios/Views/InnerCircle/CircleNavigationView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index 1d28232..1f941c9 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -42,9 +42,9 @@ struct CircleFooterView: View { .frame(width: 10, height: 10) .foregroundColor(Color.green) case .offline: - Circle() + EmptyView() default: - Circle() + EmptyView() } } .padding(5) diff --git a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift index 6e57e04..03a663c 100644 --- a/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleNavigationView.swift @@ -103,9 +103,9 @@ struct CircleNavigationView: View { .frame(width: 10, height: 10) .foregroundColor(Color.green) case .offline: - Circle() + EmptyView() default: - Circle() + EmptyView() } } } From 984cc9c3a3f6b80a226a70f3a127a7a335fc6f67 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 22:07:40 -0800 Subject: [PATCH 09/27] starting basic demo convos ui --- nirvana-ios.xcodeproj/project.pbxproj | 18 ++++++++++++++--- nirvana-ios/Views/Convos/ConvosView.swift | 20 +++++++++++++++++++ .../AgoraViewController.swift | 0 .../AgroRep.swift | 0 .../CallView.swift | 0 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 nirvana-ios/Views/Convos/ConvosView.swift rename nirvana-ios/Views/InnerCircle/{VoiceCall => VoiceCallTest}/AgoraViewController.swift (100%) rename nirvana-ios/Views/InnerCircle/{VoiceCall => VoiceCallTest}/AgroRep.swift (100%) rename nirvana-ios/Views/InnerCircle/{VoiceCall => VoiceCallTest}/CallView.swift (100%) diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 87bbc6d..1bdbb99 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 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 */; }; 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 */; }; @@ -125,6 +126,7 @@ 896CE7042770AE5D00DB474E /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = ""; }; 896CE70627711BAB00DB474E /* FindFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindFriendsView.swift; sourceTree = ""; }; 896CE70827712CED00DB474E /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + 89814682277D83F300DA46CD /* ConvosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvosView.swift; sourceTree = ""; }; 89A107E4277476CC00B971FD /* PushNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationService.swift; sourceTree = ""; }; 89A107E827753AB500B971FD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 89AD5572276DD790001658E0 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; @@ -184,14 +186,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 890C245E277BF888009B4A30 /* VoiceCall */ = { + 890C245E277BF888009B4A30 /* VoiceCallTest */ = { isa = PBXGroup; children = ( 890C245F277BF896009B4A30 /* AgoraViewController.swift */, 890C2461277BF935009B4A30 /* CallView.swift */, 890C2463277BFA97009B4A30 /* AgroRep.swift */, ); - path = VoiceCall; + path = VoiceCallTest; sourceTree = ""; }; 891060E62774460500F14509 /* Frameworks */ = { @@ -257,6 +259,7 @@ 891A15F62765639B00B26659 /* Views */ = { isa = PBXGroup; children = ( + 89814681277D83CF00DA46CD /* Convos */, 896CE7032770AE3400DB474E /* Inbox */, 894E80BF276EFCF500624B72 /* AudioPlayer */, 89F18AF3276ECF1800115B1E /* AddBasicInfo */, @@ -363,6 +366,14 @@ path = Inbox; sourceTree = ""; }; + 89814681277D83CF00DA46CD /* Convos */ = { + isa = PBXGroup; + children = ( + 89814682277D83F300DA46CD /* ConvosView.swift */, + ); + path = Convos; + sourceTree = ""; + }; 89AD5571276DD77D001658E0 /* SplashView */ = { isa = PBXGroup; children = ( @@ -392,7 +403,7 @@ 89BCABDF276B3588009E9B10 /* InnerCircle */ = { isa = PBXGroup; children = ( - 890C245E277BF888009B4A30 /* VoiceCall */, + 890C245E277BF888009B4A30 /* VoiceCallTest */, 89BCABE4276B3D58009E9B10 /* InnerCircleView.swift */, 89BCABE6276B3D64009E9B10 /* InnerCircleViewModel.swift */, 896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */, @@ -587,6 +598,7 @@ 89110714277166D6005797FA /* UserFriends.swift in Sources */, 896CE70727711BAB00DB474E /* FindFriendsView.swift in Sources */, 891A160B2765732700B26659 /* HomeViewModel.swift in Sources */, + 89814683277D83F300DA46CD /* ConvosView.swift in Sources */, 89BCABEB276D2A09009E9B10 /* PhoneVerificationView.swift in Sources */, 892179E62765FECD001E6C60 /* FooterControlsViewModel.swift in Sources */, 891A15F5276557AB00B26659 /* colors.swift in Sources */, diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift new file mode 100644 index 0000000..934c682 --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -0,0 +1,20 @@ +// +// ConvosView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import SwiftUI + +struct ConvosView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct ConvosView_Previews: PreviewProvider { + static var previews: some View { + ConvosView() + } +} diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgoraViewController.swift similarity index 100% rename from nirvana-ios/Views/InnerCircle/VoiceCall/AgoraViewController.swift rename to nirvana-ios/Views/InnerCircle/VoiceCallTest/AgoraViewController.swift diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/AgroRep.swift similarity index 100% rename from nirvana-ios/Views/InnerCircle/VoiceCall/AgroRep.swift rename to nirvana-ios/Views/InnerCircle/VoiceCallTest/AgroRep.swift diff --git a/nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift b/nirvana-ios/Views/InnerCircle/VoiceCallTest/CallView.swift similarity index 100% rename from nirvana-ios/Views/InnerCircle/VoiceCall/CallView.swift rename to nirvana-ios/Views/InnerCircle/VoiceCallTest/CallView.swift From 2ee5aabde0ac8851e975368006d41399ea5d8485 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 29 Dec 2021 23:15:58 -0800 Subject: [PATCH 10/27] ui done for one convo --- nirvana-ios.xcodeproj/project.pbxproj | 4 ++ nirvana-ios/Models/Convo.swift | 36 +++++++++++++++ nirvana-ios/Views/Convos/ConvosView.swift | 55 ++++++++++++++++++++++- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 nirvana-ios/Models/Convo.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 1bdbb99..4938b44 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 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 */; }; @@ -127,6 +128,7 @@ 896CE70627711BAB00DB474E /* FindFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindFriendsView.swift; sourceTree = ""; }; 896CE70827712CED00DB474E /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 89814682277D83F300DA46CD /* ConvosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvosView.swift; sourceTree = ""; }; + 89814684277D857300DA46CD /* Convo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Convo.swift; sourceTree = ""; }; 89A107E4277476CC00B971FD /* PushNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationService.swift; sourceTree = ""; }; 89A107E827753AB500B971FD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 89AD5572276DD790001658E0 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; @@ -328,6 +330,7 @@ 89BDCDF02769E64200577829 /* Message.swift */, 891A160D27657CEA00B26659 /* User.swift */, 89110713277166D6005797FA /* UserFriends.swift */, + 89814684277D857300DA46CD /* Convo.swift */, ); path = Models; sourceTree = ""; @@ -598,6 +601,7 @@ 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 */, diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift new file mode 100644 index 0000000..4a0812b --- /dev/null +++ b/nirvana-ios/Models/Convo.swift @@ -0,0 +1,36 @@ +// +// Convo.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import FirebaseFirestoreSwift +import FirebaseFirestore + +enum ConvoState: String, Codable { + case connected + case disconnected +} + +struct Convo: Identifiable, Codable { + @DocumentID var id: String? + var leaderUserId: String + var receiverUserId: String + + var agoraToken: String + + var state: ConvoState + + var Users: [String]? + + @ServerTimestamp var startedTimestamp: Date? + @ServerTimestamp var endedTimestamp: Date? +} + +//struct DetailConvo: Convo { +// func getRelativeStartTime() { +// +// } +//} diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 934c682..0c4e182 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -7,9 +7,39 @@ import SwiftUI +// test convos +let testConvos: [Convo] = [ + Convo(leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAIlYcWj21zCp4jq/WS2BMaiMYlKSax7ohIjzBx1BtpWAx+f9gAAAAAEADQ943gX6LOYQEAAQBfos5h", state: .connected) +] + struct ConvosView: View { var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + ScrollView([.horizontal]) { + HStack { + ForEach(0.. Date: Thu, 30 Dec 2021 01:13:10 -0800 Subject: [PATCH 11/27] basic call works with swiftui architecture --- nirvana-ios.xcodeproj/project.pbxproj | 4 + nirvana-ios/Views/Convos/ConvoViewModel.swift | 83 +++++++++++++++++++ nirvana-ios/Views/Convos/ConvosView.swift | 27 ++++-- .../Views/InnerCircle/CircleGridView.swift | 18 +--- .../Views/InnerCircle/InnerCircleView.swift | 20 +++-- .../InnerCircle/InnerCircleViewModel.swift | 3 +- 6 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 nirvana-ios/Views/Convos/ConvoViewModel.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 4938b44..2d416d4 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* 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 */; }; 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 */; }; @@ -92,6 +93,7 @@ /* Begin PBXFileReference section */ 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingOneView.swift; sourceTree = ""; }; + 89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = ""; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; @@ -373,6 +375,7 @@ isa = PBXGroup; children = ( 89814682277D83F300DA46CD /* ConvosView.swift */, + 89091992277D958D0025F4BB /* ConvoViewModel.swift */, ); path = Convos; sourceTree = ""; @@ -640,6 +643,7 @@ 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 */, diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift new file mode 100644 index 0000000..c3cf191 --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -0,0 +1,83 @@ +// +// ConvoViewModel.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/29/21. +// + +import Foundation +import AgoraRtcKit + + +class ConvoViewModel: ObservableObject { + let firestoreService = FirestoreService() + + var agoraKit: AgoraRtcEngineKit? + var agoraDelegate: AgoraRtcEngineDelegate? + + init() { + // initiate convo listener to get all relevant convos + // any convo that is active + // and any of my active friends are in that convo + // if am being "called" catch that and join the convo/channel + + // only show convos in which I know a majority of the people inside + + // initiate agora engine + self.initializeAgoraEngine() + } + + deinit { + // deinit the firestore listener + // deinit the agora engine + } + + /** + @param: friendId: the second person in the convo...the receiver + **/ + func startConvo(friendId: String) { + if let userId = AuthSessionStore.getCurrentUserId() { + + } + + // need to make sure the other user is online + + // get a new agora token from cloud function + + // create a convo/channel in db to notify the other user with proper attributes + + // join the convo myself + } + + /** + Allow user to join an active convo + */ + func joinConvo(convoId: String, convoAgoraToken: String) { + // 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 + self.leaveConvo() + + // finally join channel + agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in + + }) + } + + // anyone can leave at any time + private func leaveConvo() { + agoraKit?.leaveChannel(nil) + } + + // if you are the second to last person in the convo and you leave, you end the convo + private func endConvo() { + AgoraRtcEngineKit.destroy() + } +} + +extension ConvoViewModel { + func initializeAgoraEngine() { + // TODO: put app id in environment variables + agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: agoraDelegate) + } +} diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 0c4e182..1ad4b94 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -9,20 +9,21 @@ import SwiftUI // test convos let testConvos: [Convo] = [ - Convo(leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAIlYcWj21zCp4jq/WS2BMaiMYlKSax7ohIjzBx1BtpWAx+f9gAAAAAEADQ943gX6LOYQEAAQBfos5h", state: .connected) + Convo(id: "testChannel", leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAqbHMFow9oQ8BN0WCkFrkGBTqFTjlbr6tJmFH5judwbnZXrgMAAAAAEADQ943gC8nOYQEAAQALyc5h", state: .connected) ] struct ConvosView: View { + @ObservedObject var vm = ConvoViewModel() + @State var animate = false + var body: some View { ScrollView([.horizontal]) { HStack { ForEach(0.. Date: Thu, 30 Dec 2021 02:07:13 -0800 Subject: [PATCH 12/27] adding delegate methods and making speaker phone work --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 51 +++++++++++++++++-- nirvana-ios/Views/Convos/ConvosView.swift | 9 +++- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index c3cf191..5471299 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -9,13 +9,17 @@ import Foundation import AgoraRtcKit -class ConvoViewModel: ObservableObject { +class ConvoViewModel: NSObject, ObservableObject { + @Published var inCall = false + let firestoreService = FirestoreService() var agoraKit: AgoraRtcEngineKit? var agoraDelegate: AgoraRtcEngineDelegate? - init() { + override init() { + super.init() + // initiate convo listener to get all relevant convos // any convo that is active // and any of my active friends are in that convo @@ -58,19 +62,27 @@ class ConvoViewModel: ObservableObject { // ensure that this user leaves all other channels self.leaveConvo() + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) + // finally join channel - agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in - + self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in + self.inCall = true }) } // anyone can leave at any time - private func leaveConvo() { + func leaveConvo() { agoraKit?.leaveChannel(nil) + + // if I am the second person in the room, then end the convo + + self.inCall = false } // if you are the second to last person in the convo and you leave, you end the convo private func endConvo() { + self.leaveConvo() + AgoraRtcEngineKit.destroy() } } @@ -81,3 +93,32 @@ extension ConvoViewModel { agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: agoraDelegate) } } + +extension ConvoViewModel: AgoraRtcEngineDelegate { + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + print("I did join channel") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { + print("I did leave channel") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + print("new user joined \(uid)") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + print("user joined left") + } + + 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)") + } +} diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 1ad4b94..76eaca1 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -49,7 +49,14 @@ struct ConvosView: View { .scaleEffect(self.animate ? 1 : 0.85) .animation(animate ? Animation.easeInOut(duration: 4).repeatForever(autoreverses: true) : .default) .onTapGesture { - self.vm.joinConvo(convoId: testConvos[index].id!, convoAgoraToken: testConvos[index].agoraToken) + // if not in a call already + if !self.vm.inCall { + self.vm.joinConvo(convoId: testConvos[index].id!, convoAgoraToken: testConvos[index].agoraToken) + + return + } + + self.vm.leaveConvo() } } } From cbd655466a597422eb2439bcc9e4f107df38d6a7 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 30 Dec 2021 02:21:10 -0800 Subject: [PATCH 13/27] good spot figuring out how to use the delegate --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 5471299..56657f0 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -15,7 +15,6 @@ class ConvoViewModel: NSObject, ObservableObject { let firestoreService = FirestoreService() var agoraKit: AgoraRtcEngineKit? - var agoraDelegate: AgoraRtcEngineDelegate? override init() { super.init() @@ -60,14 +59,14 @@ class ConvoViewModel: NSObject, ObservableObject { // 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 - self.leaveConvo() + if self.inCall { + self.leaveConvo() + } self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) // finally join channel - self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: {(channel, uid, elapsed) in - self.inCall = true - }) + self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: nil) } // anyone can leave at any time @@ -90,23 +89,29 @@ class ConvoViewModel: NSObject, ObservableObject { extension ConvoViewModel { func initializeAgoraEngine() { // TODO: put app id in environment variables - agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: agoraDelegate) + agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) } } extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("I did join channel") + + // leave channel if it's just me + + self.inCall = true } func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { + // leave channel if I am the only one in it + print("I did leave channel") + + + self.inCall = false } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - // Only one remote video view is available for this - // tutorial. Here we check if there exists a surface - // view tagged as this uid. print("new user joined \(uid)") } From a9e6de6c444366cc75ebb412e0c555c6a593b66a Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 30 Dec 2021 02:49:18 -0800 Subject: [PATCH 14/27] more refactoring slowly adding in more and more --- nirvana-ios/Models/Convo.swift | 2 +- nirvana-ios/Views/Convos/ConvoViewModel.swift | 39 +++++++++++++++---- nirvana-ios/Views/Convos/ConvosView.swift | 33 ++++++++-------- .../Views/InnerCircle/InnerCircleView.swift | 8 +++- 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift index 4a0812b..e6f5dc8 100644 --- a/nirvana-ios/Models/Convo.swift +++ b/nirvana-ios/Models/Convo.swift @@ -15,7 +15,7 @@ enum ConvoState: String, Codable { } struct Convo: Identifiable, Codable { - @DocumentID var id: String? + @DocumentID var id: String? = UUID().uuidString var leaderUserId: String var receiverUserId: String diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 56657f0..bc7d13f 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -10,12 +10,20 @@ import AgoraRtcKit class ConvoViewModel: NSObject, ObservableObject { - @Published var inCall = false + @Published var selectedConvoId:String? = nil let firestoreService = FirestoreService() + let testConvos: [Convo] = [ + Convo(id: "testChannel", leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAqbHMFow9oQ8BN0WCkFrkGBTqFTjlbr6tJmFH5judwbnZXrgMAAAAAEADQ943gC8nOYQEAAQALyc5h", state: .connected) + ] + var agoraKit: AgoraRtcEngineKit? + func isInCall() -> Bool { + return self.selectedConvoId != nil + } + override init() { super.init() @@ -55,18 +63,31 @@ class ConvoViewModel: NSObject, ObservableObject { /** Allow user to join an active convo */ - func joinConvo(convoId: String, convoAgoraToken: String) { + 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.inCall { + if self.isInCall() { self.leaveConvo() } self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) + let convo = self.testConvos.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 + } + + let convoAgoraToken:String = convo!.agoraToken + let channelName:String = convo!.id! + // finally join channel - self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: "testChannel", info: nil, uid: 0, joinSuccess: nil) + self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) } // anyone can leave at any time @@ -75,7 +96,7 @@ class ConvoViewModel: NSObject, ObservableObject { // if I am the second person in the room, then end the convo - self.inCall = false + self.selectedConvoId = nil } // if you are the second to last person in the convo and you leave, you end the convo @@ -98,8 +119,6 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { print("I did join channel") // leave channel if it's just me - - self.inCall = true } func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { @@ -108,7 +127,7 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { print("I did leave channel") - self.inCall = false + self.selectedConvoId = nil } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { @@ -125,5 +144,9 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { print("did occur error: \(errorCode.rawValue)") + + // publish an error in view...toast or something + + self.selectedConvoId = nil } } diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 76eaca1..28fb038 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -7,22 +7,18 @@ import SwiftUI -// test convos -let testConvos: [Convo] = [ - Convo(id: "testChannel", leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAqbHMFow9oQ8BN0WCkFrkGBTqFTjlbr6tJmFH5judwbnZXrgMAAAAAEADQ943gC8nOYQEAAQALyc5h", state: .connected) -] - struct ConvosView: View { - @ObservedObject var vm = ConvoViewModel() + @EnvironmentObject var vm: ConvoViewModel @State var animate = false var body: some View { ScrollView([.horizontal]) { HStack { - ForEach(0.. Date: Thu, 30 Dec 2021 03:57:32 -0800 Subject: [PATCH 15/27] decent clean up --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 38 +++++-- .../Views/InnerCircle/CircleFooterView.swift | 1 + .../Views/InnerCircle/CircleGridView.swift | 99 +++++++++++++++++-- .../Views/InnerCircle/InnerCircleView.swift | 7 +- 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index bc7d13f..775f22c 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -10,6 +10,8 @@ import AgoraRtcKit class ConvoViewModel: NSObject, ObservableObject { + private var testingUIMode = true + @Published var selectedConvoId:String? = nil let firestoreService = FirestoreService() @@ -41,12 +43,23 @@ class ConvoViewModel: NSObject, ObservableObject { deinit { // deinit the firestore listener // 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) { + if testingUIMode { + print("testing mode...not doing anything") + return + } + + if let userId = AuthSessionStore.getCurrentUserId() { } @@ -64,6 +77,12 @@ class ConvoViewModel: NSObject, ObservableObject { Allow user to join an active convo */ func joinConvo() { + if testingUIMode { + print("testing mode...not doing anything") + + return + } + // 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 @@ -83,6 +102,10 @@ class ConvoViewModel: NSObject, ObservableObject { 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! @@ -92,19 +115,18 @@ class ConvoViewModel: NSObject, ObservableObject { // anyone can leave at any time func leaveConvo() { + if testingUIMode { + print("testing mode...not doing anything") + + return + } + agoraKit?.leaveChannel(nil) // if I am the second person in the room, then end the convo self.selectedConvoId = nil } - - // if you are the second to last person in the convo and you leave, you end the convo - private func endConvo() { - self.leaveConvo() - - AgoraRtcEngineKit.destroy() - } } extension ConvoViewModel { @@ -118,6 +140,8 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("I did join channel") + // set my user status to in convo + // leave channel if it's just me } diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index 1f941c9..5452d62 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -12,6 +12,7 @@ struct CircleFooterView: View { @EnvironmentObject var navigationStack: NavigationStack @EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var innerCircleVM: InnerCircleViewModel + @EnvironmentObject var convoVM: ConvoViewModel @Binding var selectedFriendIndex: String? diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 6430b79..788786b 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -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.testConvos ScrollViewReader {scrollReaderValue in ScrollView([.horizontal, .vertical], showsIndicators: false) { @@ -67,16 +71,81 @@ struct CircleGridView: View { alignment: .center, spacing: Self.spacingBetweenRows ) { + // live convos + ForEach(0.. 0 { scrollReaderValue.scrollTo(self.selectedFriendIndex) - } + } + + self.animateLiveConvos = true } } // scrollview reader } @@ -283,7 +354,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 +366,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 +388,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 diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift index 3455690..8e10ae0 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift @@ -88,13 +88,14 @@ struct InnerCircleView: View { } else { VStack(alignment: .leading) { // convos - ConvosView() - .environmentObject(self.convoViewModel) - .padding(.top, 50) +// ConvosView() +// .environmentObject(self.convoViewModel) +// .padding(.top, 50) // inner circle grid CircleGridView(selectedFriendIndex: self.$selectedFriendIndex) .environmentObject(self.innerCircleVM) + .environmentObject(self.convoViewModel) Spacer() } From 5e60dd600593c3558a5deb47123153df882dc009 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 30 Dec 2021 04:28:25 -0800 Subject: [PATCH 16/27] creating another footer to handle convos --- nirvana-ios.xcodeproj/project.pbxproj | 4 + .../Views/Convos/ConvoFooterView.swift | 97 +++++++++++++++++++ .../Views/InnerCircle/CircleFooterView.swift | 6 +- .../Views/InnerCircle/InnerCircleView.swift | 18 +--- 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 nirvana-ios/Views/Convos/ConvoFooterView.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 2d416d4..7a15982 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* 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 */; }; 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 */; }; @@ -94,6 +95,7 @@ /* Begin PBXFileReference section */ 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingOneView.swift; sourceTree = ""; }; 89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = ""; }; + 89091994277DD9F80025F4BB /* ConvoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoFooterView.swift; sourceTree = ""; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; @@ -376,6 +378,7 @@ children = ( 89814682277D83F300DA46CD /* ConvosView.swift */, 89091992277D958D0025F4BB /* ConvoViewModel.swift */, + 89091994277DD9F80025F4BB /* ConvoFooterView.swift */, ); path = Convos; sourceTree = ""; @@ -632,6 +635,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 */, diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift new file mode 100644 index 0000000..605476c --- /dev/null +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -0,0 +1,97 @@ +// +// 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 { + ProfilePictureOverlap() + + // meta data of convo + VStack (alignment: .leading) { + Text("kev, sarth, and 5 others") + .font(.footnote) + .foregroundColor(NirvanaColor.light) + + 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) + .padding() + .font(.title2) + } + + // 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) + .padding() + .font(.title2) + } + + } + .frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle + .background(Color.white.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous)) + .shadow(color: Color.black.opacity(0.25), radius: 30, x: 0, y: 20) + } + } + .padding() + .animation(Animation.spring(), value: self.convoVM.selectedConvoId) + .offset( + x:0, + y:self.convoVM.selectedConvoId == nil ? 150 : 0 + ) + .onChange(of: self.convoVM.selectedConvoId) {newConvo in + // update meta data based on which convo was selected + + // reset the selected options to start fresh +// self.convoRelativeTime = "" + + // get relative start time of convo +// let formatter = RelativeDateTimeFormatter() +// formatter.unitsStyle = .full +// +// let relativeDate = formatter.localizedString(for: ?.sentTimestamp ?? Date(), relativeTo: Date()) + } + } +} + +struct ConvoFooterView_Previews: PreviewProvider { + static var previews: some View { + ConvoFooterView() + } +} diff --git a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift index 5452d62..82d6d69 100644 --- a/nirvana-ios/Views/InnerCircle/CircleFooterView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleFooterView.swift @@ -12,7 +12,6 @@ struct CircleFooterView: View { @EnvironmentObject var navigationStack: NavigationStack @EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var innerCircleVM: InnerCircleViewModel - @EnvironmentObject var convoVM: ConvoViewModel @Binding var selectedFriendIndex: String? @@ -131,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!] diff --git a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift index 8e10ae0..60e7fe2 100644 --- a/nirvana-ios/Views/InnerCircle/InnerCircleView.swift +++ b/nirvana-ios/Views/InnerCircle/InnerCircleView.swift @@ -86,19 +86,11 @@ struct InnerCircleView: View { } .padding() } else { - VStack(alignment: .leading) { - // convos -// ConvosView() -// .environmentObject(self.convoViewModel) -// .padding(.top, 50) - - // inner circle grid - CircleGridView(selectedFriendIndex: self.$selectedFriendIndex) - .environmentObject(self.innerCircleVM) - .environmentObject(self.convoViewModel) - - Spacer() - } + // inner circle grid + CircleGridView(selectedFriendIndex: self.$selectedFriendIndex) + .environmentObject(self.innerCircleVM) + .environmentObject(self.convoViewModel) + } // header From 57c9b2dd46d37a26943f416a7f94c627f27f17c8 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 30 Dec 2021 04:46:36 -0800 Subject: [PATCH 17/27] good spot with footer stuff --- nirvana-ios.xcodeproj/project.pbxproj | 4 ++ .../Views/Convos/ConvoFooterView.swift | 9 +++-- nirvana-ios/Views/Convos/ConvosView.swift | 24 +----------- .../ProfilePicturesOverlappedView.swift | 38 +++++++++++++++++++ .../Views/InnerCircle/CircleGridView.swift | 2 +- .../Views/InnerCircle/InnerCircleView.swift | 5 ++- 6 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 nirvana-ios/Views/Convos/ProfilePicturesOverlappedView.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 7a15982..c540f14 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 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 */; }; 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 */; }; @@ -96,6 +97,7 @@ 8904DEC7277055E90071E6CA /* OnboardingOneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingOneView.swift; sourceTree = ""; }; 89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = ""; }; 89091994277DD9F80025F4BB /* ConvoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoFooterView.swift; sourceTree = ""; }; + 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = ""; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; @@ -379,6 +381,7 @@ 89814682277D83F300DA46CD /* ConvosView.swift */, 89091992277D958D0025F4BB /* ConvoViewModel.swift */, 89091994277DD9F80025F4BB /* ConvoFooterView.swift */, + 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */, ); path = Convos; sourceTree = ""; @@ -600,6 +603,7 @@ 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 */, diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift index 605476c..a1998be 100644 --- a/nirvana-ios/Views/Convos/ConvoFooterView.swift +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -18,7 +18,10 @@ struct ConvoFooterView: View { Spacer() HStack { - ProfilePictureOverlap() + ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35)) + .padding(.leading, 20) + .padding(.trailing, 10) + // meta data of convo VStack (alignment: .leading) { @@ -43,8 +46,8 @@ struct ConvoFooterView: View { Label("attendees", systemImage: "person.2.circle.fill") .labelStyle(.iconOnly) .foregroundColor(NirvanaColor.teal) - .padding() .font(.title2) + .padding(.trailing, 5) } // disconnect button @@ -58,8 +61,8 @@ struct ConvoFooterView: View { Label("Call", systemImage: "powerplug.fill") .labelStyle(.iconOnly) .foregroundColor(Color.orange) - .padding() .font(.title2) + .padding(.trailing, 5) } } diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 28fb038..555eb15 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -22,7 +22,7 @@ struct ConvosView: View { ZStack { Color.clear - ProfilePictureOverlap() + ProfilePicturesOverlappedView() .shadow(radius: 10) } @@ -74,25 +74,3 @@ struct ConvosView: View { // ConvosView() // } //} - -struct ProfilePictureOverlap: View { - let numberAttendees:Int = 5 - let offset:Int = 15 - - var body: some View { - ZStack { - ForEach(0.. Date: Thu, 30 Dec 2021 05:01:48 -0800 Subject: [PATCH 18/27] adding call states based on connection --- .../Views/Convos/ConvoFooterView.swift | 27 +++++++++++++++++++ nirvana-ios/Views/Convos/ConvoViewModel.swift | 8 ++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift index a1998be..98b645b 100644 --- a/nirvana-ios/Views/Convos/ConvoFooterView.swift +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -29,6 +29,33 @@ struct ConvoFooterView: View { .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) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 775f22c..7e10b53 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -8,11 +8,11 @@ import Foundation import AgoraRtcKit - class ConvoViewModel: NSObject, ObservableObject { private var testingUIMode = true @Published var selectedConvoId:String? = nil + @Published var connectionState: AgoraConnectionStateType? = nil let firestoreService = FirestoreService() @@ -137,9 +137,13 @@ extension ConvoViewModel { } 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") - + // set my user status to in convo // leave channel if it's just me From 699288e17f1ac65cada190b4460b9adb4319a4c7 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 30 Dec 2021 21:12:54 -0800 Subject: [PATCH 19/27] compiling but haven't tested anything --- nirvana-ios/Models/Convo.swift | 11 +- nirvana-ios/Models/User.swift | 1 + .../Repositories/FirestoreService.swift | 30 +++ nirvana-ios/Services/AgoraService.swift | 29 ++- .../Views/Convos/ConvoFooterView.swift | 1 + nirvana-ios/Views/Convos/ConvoViewModel.swift | 223 ++++++++++++++---- nirvana-ios/Views/Convos/ConvosView.swift | 6 +- .../ProfilePicturesOverlappedView.swift | 1 - .../Views/InnerCircle/CircleGridView.swift | 22 +- .../InnerCircle/CircleNavigationView.swift | 16 +- .../InnerCircle/InnerCircleViewModel.swift | 2 +- 11 files changed, 276 insertions(+), 66 deletions(-) diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift index e6f5dc8..333cd63 100644 --- a/nirvana-ios/Models/Convo.swift +++ b/nirvana-ios/Models/Convo.swift @@ -10,12 +10,15 @@ import FirebaseFirestoreSwift import FirebaseFirestore enum ConvoState: String, Codable { - case connected - case disconnected + case initialized + case active + case complete } struct Convo: Identifiable, Codable { + // channel name for agora's purposes @DocumentID var id: String? = UUID().uuidString + var leaderUserId: String var receiverUserId: String @@ -23,10 +26,10 @@ struct Convo: Identifiable, Codable { var state: ConvoState - var Users: [String]? + var users: [String] = [] @ServerTimestamp var startedTimestamp: Date? - @ServerTimestamp var endedTimestamp: Date? + var endedTimestamp: Date? } //struct DetailConvo: Convo { diff --git a/nirvana-ios/Models/User.swift b/nirvana-ios/Models/User.swift index fe508a3..54e2926 100644 --- a/nirvana-ios/Models/User.swift +++ b/nirvana-ios/Models/User.swift @@ -14,6 +14,7 @@ enum UserStatus: String, Codable { case online case offline case background + case inConvo } struct User: Identifiable, Codable { diff --git a/nirvana-ios/Repositories/FirestoreService.swift b/nirvana-ios/Repositories/FirestoreService.swift index 0b6be0f..e9381b5 100644 --- a/nirvana-ios/Repositories/FirestoreService.swift +++ b/nirvana-ios/Repositories/FirestoreService.swift @@ -16,6 +16,7 @@ class FirestoreService { case users = "users" case messages = "messages" case userFriends = "user_friends" // associating a user to + case convos = "convos" } private var db = Firestore.firestore() @@ -183,3 +184,32 @@ extension FirestoreService { } } } + + +// convos +extension FirestoreService { + func createConvo(convo: Convo, completion: @escaping((_ state: ServiceState) -> ())) { + do { + let _ = try db.collection(Collection.convos.rawValue).addDocument(from: convo) + + completion(ServiceState.success("convo created")) + } catch { + print("error in creating convo \(error.localizedDescription)") + completion(ServiceState.error(ServiceError(description: error.localizedDescription))) + } + } + + func updateConvo(convo: Convo, completion: @escaping((_ state: ServiceState) -> ())) { + do { + if convo.id != nil { + let _ = try db.collection(Collection.convos.rawValue).document(convo.id!).setData(from: convo) + completion(ServiceState.success("Updated convo in firestore service")) + } else { + completion(ServiceState.error(ServiceError(description: "No convo id given to firestore service"))) + } + } catch { + print("error in updating convo \(error.localizedDescription)") + completion(ServiceState.error(ServiceError(description: error.localizedDescription))) + } + } +} diff --git a/nirvana-ios/Services/AgoraService.swift b/nirvana-ios/Services/AgoraService.swift index 026b213..2895c59 100644 --- a/nirvana-ios/Services/AgoraService.swift +++ b/nirvana-ios/Services/AgoraService.swift @@ -10,7 +10,9 @@ import FirebaseAuth import Firebase class AgoraService { - func getAgoraUserToken(channelName: String) { + lazy var functions = Functions.functions() + + func getAgoraUserTokenServer(channelName: String) { let currentUser = Auth.auth().currentUser currentUser?.getIDTokenForcingRefresh(true) { idToken, error in if let error = error { @@ -45,4 +47,29 @@ class AgoraService { 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(message) + } + completion(nil) + } + + if let data = result?.data as? [String: Any], let token = data["token"] as? String { + print("got agora token from cloud function") + completion(token) + } else { + completion(nil) + } + } + } } diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift index 98b645b..8474ce1 100644 --- a/nirvana-ios/Views/Convos/ConvoFooterView.swift +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -81,6 +81,7 @@ struct ConvoFooterView: View { Button { print("disconnecting user now ") + self.convoVM.selectedConvoId = nil self.convoVM.leaveConvo() // show success or failure toast diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 7e10b53..b30144e 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -7,18 +7,16 @@ import Foundation import AgoraRtcKit +import Firebase class ConvoViewModel: NSObject, ObservableObject { - private var testingUIMode = true - @Published var selectedConvoId:String? = nil @Published var connectionState: AgoraConnectionStateType? = nil let firestoreService = FirestoreService() + let agoraService = AgoraService() - let testConvos: [Convo] = [ - Convo(id: "testChannel", leaderUserId: "VWVPKCrNmMeZlkEeZxuJ0Wsgq0C3", receiverUserId: "zd6PpD3TmnQUDoy6noS9aZpuPWr1", agoraToken: "006c8dfd65deb5c4741bd564085627139d0IAAqbHMFow9oQ8BN0WCkFrkGBTqFTjlbr6tJmFH5judwbnZXrgMAAAAAEADQ943gC8nOYQEAAQALyc5h", state: .connected) - ] + @Published var relevantConvos: [Convo] = [] var agoraKit: AgoraRtcEngineKit? @@ -26,15 +24,68 @@ class ConvoViewModel: NSObject, ObservableObject { return self.selectedConvoId != nil } - override init() { - super.init() - + var convosListener: ListenerRegistration? = nil + + private var db = Firestore.firestore() + + private var allConvos: [Convo] = [] + + private var relevancyAcceptance = 0.6 + + func activateDataListener(activeFriendsIds: [String]) { // initiate convo listener to get all relevant convos // any convo that is active - // and any of my active friends are in that convo + // and any of my active friends or I am in that convo // if am being "called" catch that and join the convo/channel + var scopeUserIds: [String] = [] + scopeUserIds += activeFriendsIds - // only show convos in which I know a majority of the people inside + // I want to get back convos that involve me + if let userId = AuthSessionStore.getCurrentUserId() { + scopeUserIds.append(userId) + } + + print("the scope of users to search for convos is \(scopeUserIds)") + + self.convosListener = db.collection("convos").whereField("state", isEqualTo: ConvoState.active.rawValue).whereField("users", arrayContainsAny: scopeUserIds) + .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 + } + + 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 + } + } + + if Double(relevancyCount / convo.users.count) > self.relevancyAcceptance { + print("this convo counts as relevant!!!") + return true + } + return false + } + } // initiate agora engine self.initializeAgoraEngine() @@ -42,6 +93,8 @@ class ConvoViewModel: NSObject, ObservableObject { deinit { // deinit the firestore listener + self.convosListener?.remove() + // deinit the agora engine print("deiniting and cleaning up convo view model/agora services") @@ -54,35 +107,55 @@ class ConvoViewModel: NSObject, ObservableObject { @param: friendId: the second person in the convo...the receiver **/ func startConvo(friendId: String) { - if testingUIMode { - print("testing mode...not doing anything") - return - } - + // 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") + } + + let users = [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 + + var 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(channelName: channelName, agoraToken: token!) + case .error(let error): + print(error) + default: + return + } + } + } + } + } + + func joinConvo(channelName: String, agoraToken: String) { + // ensure that this user leaves all other channels + if self.isInCall() { + self.leaveConvo() } - // need to make sure the other user is online - - // get a new agora token from cloud function - - // create a convo/channel in db to notify the other user with proper attributes - - // join the convo myself + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) + // finally join channel + self.agoraKit?.joinChannel(byToken: agoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) } + /** Allow user to join an active convo */ func joinConvo() { - if testingUIMode { - print("testing mode...not doing anything") - - return - } - // 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 @@ -90,9 +163,7 @@ class ConvoViewModel: NSObject, ObservableObject { self.leaveConvo() } - self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) - - let convo = self.testConvos.first {convo in + let convo = self.relevantConvos.first {convo in convo.id == self.selectedConvoId } @@ -102,25 +173,19 @@ class ConvoViewModel: NSObject, ObservableObject { 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?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) } // anyone can leave at any time func leaveConvo() { - if testingUIMode { - print("testing mode...not doing anything") - - return - } - agoraKit?.leaveChannel(nil) // if I am the second person in the room, then end the convo @@ -143,19 +208,87 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("I did join channel") - - // set my user status to in convo - // leave channel if it's just me + // TODO: play a sound when I join the channel + + // TODO: leave channel if it's just me + + + var convo = self.relevantConvos.first {convo in + convo.id == self.selectedConvoId + } + if convo == nil { + print("no such convo found") + return + } + + if let userId = AuthSessionStore.getCurrentUserId() { + // set my user status to in convo/red + 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 there + + convo?.users.append(userId) + convo?.state = .active + + self?.firestoreService.updateConvo(convo: convo!) {[weak self] res in + print(res) + } + case .error(let error): + print(error) + default: + return + } + } + } } func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { - // leave channel if I am the only one in it - print("I did leave channel") + var convo = self.relevantConvos.first {convo in + convo.id == self.selectedConvoId + } + if convo == nil { + print("no such convo found") + return + } - self.selectedConvoId = nil + if let userId = AuthSessionStore.getCurrentUserId() { + // set my user status to online + self.firestoreService.updateUserStatus(userId: userId, userStatus: .online) {[weak self] res in + print(res) + + switch res { + case .success: + //update convo in database to show that I left...if I am the last person, also close out the channel + + let updatedUsers: [String] = convo!.users.filter{ arrUserId in + return arrUserId != userId + } + + convo!.users = updatedUsers + + // if I am the last one in the convo, then end the convo + if convo!.users.count == 1 && convo!.users[0] == userId { + convo!.state = .complete + convo!.endedTimestamp = Date() + } + + self?.firestoreService.updateConvo(convo: convo!) {[weak self] res in + self?.selectedConvoId = nil + print("left convo officially in our db as well") + } + case .error(let error): + print(error) + default: + return + } + } + } } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index 555eb15..d5dff04 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -14,8 +14,8 @@ struct ConvosView: View { var body: some View { ScrollView([.horizontal]) { HStack { - ForEach(0.. Date: Thu, 30 Dec 2021 22:54:01 -0800 Subject: [PATCH 20/27] getting closer and closer --- nirvana-ios/Services/AgoraService.swift | 4 +- nirvana-ios/Views/Convos/ConvoViewModel.swift | 94 ++++++++++--------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/nirvana-ios/Services/AgoraService.swift b/nirvana-ios/Services/AgoraService.swift index 2895c59..dc400c5 100644 --- a/nirvana-ios/Services/AgoraService.swift +++ b/nirvana-ios/Services/AgoraService.swift @@ -58,12 +58,12 @@ class AgoraService { let code = FunctionsErrorCode(rawValue: error.code) let message = error.localizedDescription let details = error.userInfo[FunctionsErrorDetailsKey] - - print(message) } + 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) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index b30144e..28a2b29 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -14,22 +14,20 @@ class ConvoViewModel: NSObject, ObservableObject { @Published var connectionState: AgoraConnectionStateType? = nil let firestoreService = FirestoreService() + private var db = Firestore.firestore() + var convosListener: ListenerRegistration? = nil + let agoraService = AgoraService() - - @Published var relevantConvos: [Convo] = [] - var agoraKit: AgoraRtcEngineKit? + private var allConvos: [Convo] = [] + @Published var relevantConvos: [Convo] = [] + private var currConvo: Convo? = nil // local cache for updating + func isInCall() -> Bool { return self.selectedConvoId != nil } - var convosListener: ListenerRegistration? = nil - - private var db = Firestore.firestore() - - private var allConvos: [Convo] = [] - private var relevancyAcceptance = 0.6 func activateDataListener(activeFriendsIds: [String]) { @@ -65,6 +63,8 @@ class ConvoViewModel: NSObject, ObservableObject { continue } + // TODO: if convo receiver is me, then join in + self.allConvos.append(convo!) } @@ -86,9 +86,6 @@ class ConvoViewModel: NSObject, ObservableObject { return false } } - - // initiate agora engine - self.initializeAgoraEngine() } deinit { @@ -117,19 +114,22 @@ class ConvoViewModel: NSObject, ObservableObject { self.agoraService.getAgoraTokenCF(channelName: channelName) {[weak self] token in if token == nil { print("error in getting token") + return } - let users = [userId] // just one user now, and that's me...but no one has joined yet + 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 - var convo = Convo(id: channelName, leaderUserId: userId, receiverUserId: friendId, agoraToken: token!, state: .initialized, users: [], startedTimestamp: nil, endedTimestamp: nil) + 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(channelName: channelName, agoraToken: token!) + self?.joinConvo(convo: convo) case .error(let error): print(error) default: @@ -140,15 +140,23 @@ class ConvoViewModel: NSObject, ObservableObject { } } - func joinConvo(channelName: String, agoraToken: String) { + func joinConvo(convo: Convo) { // ensure that this user leaves all other channels if self.isInCall() { self.leaveConvo() } + // add this convo for the delegate use + self.currConvo = convo + + // update ui to select this one + self.selectedConvoId = convo.id + + self.initializeAgoraEngine() // give delegate up to date object + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) // finally join channel - self.agoraKit?.joinChannel(byToken: agoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) + self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil) } @@ -173,12 +181,16 @@ class ConvoViewModel: NSObject, ObservableObject { return } + self.currConvo = convo + // 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.initializeAgoraEngine() + self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) // finally join channel self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) @@ -191,6 +203,9 @@ class ConvoViewModel: NSObject, ObservableObject { // if I am the second person in the room, then end the convo self.selectedConvoId = nil + self.currConvo = nil + + AgoraRtcEngineKit.destroy() } } @@ -213,28 +228,25 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { // TODO: leave channel if it's just me - - var convo = self.relevantConvos.first {convo in - convo.id == self.selectedConvoId - } - if convo == nil { - print("no such convo found") - return - } - if let userId = AuthSessionStore.getCurrentUserId() { - // set my user status to in convo/red + + if self.currConvo == nil { + print("no such convo found") + return + } + + // set my user status to in convo 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 there + // update convo in database users array to let them know I am in now - convo?.users.append(userId) - convo?.state = .active + self?.currConvo!.users.append(userId) + self?.currConvo!.state = .active - self?.firestoreService.updateConvo(convo: convo!) {[weak self] res in + self?.firestoreService.updateConvo(convo: (self?.currConvo)!) {[weak self] res in print(res) } case .error(let error): @@ -249,10 +261,7 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { print("I did leave channel") - var convo = self.relevantConvos.first {convo in - convo.id == self.selectedConvoId - } - if convo == nil { + if self.currConvo == nil { print("no such convo found") return } @@ -266,20 +275,21 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { case .success: //update convo in database to show that I left...if I am the last person, also close out the channel - let updatedUsers: [String] = convo!.users.filter{ arrUserId in + let updatedUsers: [String] = (self?.currConvo!.users.filter{ arrUserId in return arrUserId != userId - } + })! - convo!.users = updatedUsers + self?.currConvo!.users = updatedUsers // if I am the last one in the convo, then end the convo - if convo!.users.count == 1 && convo!.users[0] == userId { - convo!.state = .complete - convo!.endedTimestamp = Date() + if self?.currConvo!.users.count == 1 && self?.currConvo!.users[0] == userId { + self?.currConvo!.state = .complete + self?.currConvo!.endedTimestamp = Date() } - self?.firestoreService.updateConvo(convo: convo!) {[weak self] res in + self?.firestoreService.updateConvo(convo: (self?.currConvo)!) {[weak self] res in self?.selectedConvoId = nil + self?.currConvo = nil print("left convo officially in our db as well") } case .error(let error): From c879f916e5269128a93202c0d729c9d8e46b8e4f Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 00:30:36 -0800 Subject: [PATCH 21/27] decent place? --- .../Assets.xcassets/sounds/Contents.json | 6 + .../[5]__Notify__Sound.dataset/Contents.json | 12 ++ .../[5]__Notify__Sound.mp3 | Bin 0 -> 29407 bytes .../[8]__Notify__Sound.dataset/Contents.json | 12 ++ .../[8]__Notify__Sound.mp3 | Bin 0 -> 52604 bytes nirvana-ios/Models/Convo.swift | 2 +- .../Repositories/FirestoreService.swift | 2 +- nirvana-ios/Views/Convos/ConvoViewModel.swift | 187 ++++++++++++------ .../Views/InnerCircle/CircleGridView.swift | 3 - 9 files changed, 163 insertions(+), 61 deletions(-) create mode 100644 nirvana-ios/Assets.xcassets/sounds/Contents.json create mode 100644 nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json create mode 100644 nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 create mode 100644 nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/Contents.json create mode 100644 nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/[8]__Notify__Sound.mp3 diff --git a/nirvana-ios/Assets.xcassets/sounds/Contents.json b/nirvana-ios/Assets.xcassets/sounds/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/nirvana-ios/Assets.xcassets/sounds/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json b/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json new file mode 100644 index 0000000..3c97b2b --- /dev/null +++ b/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "[5]__Notify__Sound.mp3", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 b/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef172707e1aadbcdb31f3140d9054c1e726c4c28 GIT binary patch literal 29407 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 literal 0 HcmV?d00001 diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift index 333cd63..a054175 100644 --- a/nirvana-ios/Models/Convo.swift +++ b/nirvana-ios/Models/Convo.swift @@ -17,7 +17,7 @@ enum ConvoState: String, Codable { struct Convo: Identifiable, Codable { // channel name for agora's purposes - @DocumentID var id: String? = UUID().uuidString + @DocumentID var id: String? var leaderUserId: String var receiverUserId: String diff --git a/nirvana-ios/Repositories/FirestoreService.swift b/nirvana-ios/Repositories/FirestoreService.swift index e9381b5..a3ed05c 100644 --- a/nirvana-ios/Repositories/FirestoreService.swift +++ b/nirvana-ios/Repositories/FirestoreService.swift @@ -190,7 +190,7 @@ extension FirestoreService { extension FirestoreService { func createConvo(convo: Convo, completion: @escaping((_ state: ServiceState) -> ())) { do { - let _ = try db.collection(Collection.convos.rawValue).addDocument(from: convo) + let _ = try db.collection(Collection.convos.rawValue).document(convo.id!).setData(from: convo) completion(ServiceState.success("convo created")) } catch { diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 28a2b29..1c20916 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -28,64 +28,95 @@ class ConvoViewModel: NSObject, ObservableObject { return self.selectedConvoId != nil } - private var relevancyAcceptance = 0.6 + static var relevancyAcceptance = 0.6 - func activateDataListener(activeFriendsIds: [String]) { - // 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 - var scopeUserIds: [String] = [] - scopeUserIds += activeFriendsIds + override init() { + super.init() - // I want to get back convos that involve me - if let userId = AuthSessionStore.getCurrentUserId() { - scopeUserIds.append(userId) + // start process of data collection + let userId = AuthSessionStore.getCurrentUserId() + if userId == nil { + print("no authenticated user") } - print("the scope of users to search for convos is \(scopeUserIds)") + // get all active userfriends + var scopeUserIds: [String] = [] + scopeUserIds.append(userId!) - self.convosListener = db.collection("convos").whereField("state", isEqualTo: ConvoState.active.rawValue).whereField("users", arrayContainsAny: scopeUserIds) - .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 - } - - // TODO: if convo receiver is me, then join in - - 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 + 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) } - if Double(relevancyCount / convo.users.count) > self.relevancyAcceptance { - print("this convo counts as relevant!!!") - return true - } - return false + + //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 + } + + // TODO: if convo receiver is me, then join in + if convo!.receiverUserId == userId { + self?.joinConvo(convo: convo!) + } + + 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 { @@ -122,8 +153,6 @@ class ConvoViewModel: NSObject, ObservableObject { 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 { @@ -168,7 +197,8 @@ class ConvoViewModel: NSObject, ObservableObject { // ensure that this user leaves all other channels if self.isInCall() { - self.leaveConvo() + print("can't join another call, already in one") + return } let convo = self.relevantConvos.first {convo in @@ -188,7 +218,6 @@ class ConvoViewModel: NSObject, ObservableObject { let convoAgoraToken:String = convo!.agoraToken let channelName:String = convo!.id! - self.initializeAgoraEngine() self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) @@ -214,6 +243,50 @@ extension ConvoViewModel { // TODO: put app id in environment variables agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) } + + func playJoinAudioEffect(engine: AgoraRtcEngineKit) { + // Sets the audio effect ID. + let EFFECT_ID:Int32 = 1 + // Sets the path of the audio effect file. + let filePath = Bundle.main.path(forResource: "[8]__Notify__Sound", 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 = true + // 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) { + // Sets the audio effect ID. + let EFFECT_ID:Int32 = 2 + // Sets the path of the audio effect file. + let filePath = Bundle.main.path(forResource: "[5]__Notify__Sound", 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 = true + // 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 { @@ -224,12 +297,11 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("I did join channel") - // TODO: play a sound when I join the channel - + self.playJoinAudioEffect(engine: engine) + // TODO: leave channel if it's just me if let userId = AuthSessionStore.getCurrentUserId() { - if self.currConvo == nil { print("no such convo found") return @@ -261,6 +333,9 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { print("I did leave channel") + // sound effect upon leaving + self.playLeaveAudioEffect(engine: engine) + if self.currConvo == nil { print("no such convo found") return diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index c45bfca..39a282b 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -332,9 +332,6 @@ struct CircleGridView: View { } self.animateLiveConvos = true - - // TODO: called once, have to refresh page to make it update on new friends added and such - self.convoVM.activateDataListener(activeFriendsIds: self.authSessionStore.friendsArr) } } // scrollview reader } From 71fa444cf234b467d72fa6267d2a377fc46a35a3 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 01:02:20 -0800 Subject: [PATCH 22/27] getting sounds working and getting both connected, but the leave channel not triggering --- nirvana-ios.xcodeproj/project.pbxproj | 16 +++++++++++++ .../Contents.json | 2 +- .../joinSound.mp3} | Bin .../Contents.json | 2 +- .../leaveSound.mp3} | Bin .../Views/Convos/ConvoFooterView.swift | 1 - nirvana-ios/Views/Convos/ConvoViewModel.swift | 22 +++++++++++++----- .../Views/InnerCircle/CircleGridView.swift | 2 +- nirvana-ios/sounds/joinSound.mp3 | Bin 0 -> 52604 bytes nirvana-ios/sounds/leaveSound.mp3 | Bin 0 -> 29407 bytes 10 files changed, 35 insertions(+), 10 deletions(-) rename nirvana-ios/Assets.xcassets/sounds/{[5]__Notify__Sound.dataset => joinSound.dataset}/Contents.json (72%) rename nirvana-ios/Assets.xcassets/sounds/{[8]__Notify__Sound.dataset/[8]__Notify__Sound.mp3 => joinSound.dataset/joinSound.mp3} (100%) rename nirvana-ios/Assets.xcassets/sounds/{[8]__Notify__Sound.dataset => leaveSound.dataset}/Contents.json (72%) rename nirvana-ios/Assets.xcassets/sounds/{[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 => leaveSound.dataset/leaveSound.mp3} (100%) create mode 100644 nirvana-ios/sounds/joinSound.mp3 create mode 100644 nirvana-ios/sounds/leaveSound.mp3 diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index c540f14..b6152cb 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */; }; @@ -98,6 +100,8 @@ 89091992277D958D0025F4BB /* ConvoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoViewModel.swift; sourceTree = ""; }; 89091994277DD9F80025F4BB /* ConvoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoFooterView.swift; sourceTree = ""; }; 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = ""; }; + 89091998277EFDCC0025F4BB /* leaveSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = leaveSound.mp3; sourceTree = ""; }; + 8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = ""; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = ""; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = ""; }; @@ -194,6 +198,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 8909199A277EFDE50025F4BB /* sounds */ = { + isa = PBXGroup; + children = ( + 8909199B277EFDF30025F4BB /* joinSound.mp3 */, + 89091998277EFDCC0025F4BB /* leaveSound.mp3 */, + ); + path = sounds; + sourceTree = ""; + }; 890C245E277BF888009B4A30 /* VoiceCallTest */ = { isa = PBXGroup; children = ( @@ -231,6 +244,7 @@ 891A15DF27653A7000B26659 /* nirvana-ios */ = { isa = PBXGroup; children = ( + 8909199A277EFDE50025F4BB /* sounds */, 891060E527743FEB00F14509 /* nirvana-ios.entitlements */, 89AD557D276E01E2001658E0 /* Repositories */, 89F05BA2276ACBFA0002E9C7 /* Services */, @@ -587,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; diff --git a/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json similarity index 72% rename from nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json rename to nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json index 3c97b2b..58de6d7 100644 --- a/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/Contents.json +++ b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/Contents.json @@ -1,7 +1,7 @@ { "data" : [ { - "filename" : "[5]__Notify__Sound.mp3", + "filename" : "joinSound.mp3", "idiom" : "universal" } ], diff --git a/nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/[8]__Notify__Sound.mp3 b/nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/joinSound.mp3 similarity index 100% rename from nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/[8]__Notify__Sound.mp3 rename to nirvana-ios/Assets.xcassets/sounds/joinSound.dataset/joinSound.mp3 diff --git a/nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/Contents.json b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json similarity index 72% rename from nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/Contents.json rename to nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json index 5b12314..8ec16f8 100644 --- a/nirvana-ios/Assets.xcassets/sounds/[8]__Notify__Sound.dataset/Contents.json +++ b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/Contents.json @@ -1,7 +1,7 @@ { "data" : [ { - "filename" : "[8]__Notify__Sound.mp3", + "filename" : "leaveSound.mp3", "idiom" : "universal" } ], diff --git a/nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 b/nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/leaveSound.mp3 similarity index 100% rename from nirvana-ios/Assets.xcassets/sounds/[5]__Notify__Sound.dataset/[5]__Notify__Sound.mp3 rename to nirvana-ios/Assets.xcassets/sounds/leaveSound.dataset/leaveSound.mp3 diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift index 8474ce1..98b645b 100644 --- a/nirvana-ios/Views/Convos/ConvoFooterView.swift +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -81,7 +81,6 @@ struct ConvoFooterView: View { Button { print("disconnecting user now ") - self.convoVM.selectedConvoId = nil self.convoVM.leaveConvo() // show success or failure toast diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 1c20916..8852075 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -87,7 +87,7 @@ class ConvoViewModel: NSObject, ObservableObject { } // TODO: if convo receiver is me, then join in - if convo!.receiverUserId == userId { + if convo!.receiverUserId == userId && !(self?.isInCall())! { self?.joinConvo(convo: convo!) } @@ -245,10 +245,12 @@ 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: "[8]__Notify__Sound", ofType: "mp3") + 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. @@ -259,7 +261,7 @@ extension ConvoViewModel { // 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 = true + 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. @@ -267,10 +269,12 @@ extension ConvoViewModel { } 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: "[5]__Notify__Sound", ofType: "mp3") + 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. @@ -281,7 +285,7 @@ extension ConvoViewModel { // 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 = true + 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. @@ -320,6 +324,8 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { self?.firestoreService.updateConvo(convo: (self?.currConvo)!) {[weak self] res in print(res) + + } case .error(let error): print(error) @@ -378,10 +384,14 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { 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("user joined left") + print("a user left") + + self.playLeaveAudioEffect(engine: engine) } func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { diff --git a/nirvana-ios/Views/InnerCircle/CircleGridView.swift b/nirvana-ios/Views/InnerCircle/CircleGridView.swift index 39a282b..14bfcd2 100644 --- a/nirvana-ios/Views/InnerCircle/CircleGridView.swift +++ b/nirvana-ios/Views/InnerCircle/CircleGridView.swift @@ -331,7 +331,7 @@ struct CircleGridView: View { scrollReaderValue.scrollTo(self.selectedFriendIndex) } - self.animateLiveConvos = true + self.animateLiveConvos = true } } // scrollview reader } diff --git a/nirvana-ios/sounds/joinSound.mp3 b/nirvana-ios/sounds/joinSound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4a42d9b19a2b55d0054d4f0c7ca42cc0f2f20536 GIT binary patch literal 52604 zcmeFZcT`l*x+c84X>tQi&eG%zP0rfnAUQ|LIR`;Nq=^!hEMOq#EC?bXP0ksVC?H8d zRI-B5gXi3P&OP_LGi&awnZLeYt+n^6+V$?Ys-Al4t7@8^T-$Zb3 zE@6bQ5MZdRt#}isyty>xwUptmUT_snxIw^u2Ul;nFv3tlSL-GVa&rlZ2pKBL8{WkJ zyo|UBfUdr}F#tfn1AwWPE+Bj7fsd`dyJG;{&e7S`OOEAxTPF+L)j^KML`(~z<)i56 z;;I(n=V%b3ZD=3jZZGA)a_2UI>;vfso<5$A0k-f5o*rKQ(huZVY#qGq9HnpKe;y06 z!2c!^;4a4^|0e@}Qy5&)+s_d$E`Z>-M<9gZLPEj-K6KP?of0fqKl2-Tf53u#JcT`uBW4Wn_fUB#6G(ue5 zPEy#>iQh@YR*c_CLe!pL%Ffo2Usy`QUR26f%uY;F1HYZ9m?*yk!bwER0bwU(EA)>#->A^>bG@Odt;hedsy|eK z%ic8LMwyzce}K1N@ZTQ&ogW5{zJE{sJ?Y{4H)6oyf9o}ATl+u6kz?_<4Rmz)Tf1&_ z{XNU|zd*-M#7Rs_LQ>)e6MG4MaWPQ@zpb#8Fu#O^ot=}tor8$of7TM@;&`L}Pq+R{ z-~CH({G&*F7h5l9$D5HV$RhZEuhRe9UHXrD{7)+VFNFJt@INd4$4va;>%UiCR`Ab& z{Pzx!75x8n(ccCB!w9}<^xvP|ScV&$B=`@b^lxa~6#D-%{2Mj?Ulsv}|5eC;3*W!s z`U|fA76Sh*;lJ4R7hL}>1pZsXf3fTT8e9bb+}1dH-K>y;ZZ<79`=7tUUkLn#z+VXb zg}`43{Dr{(-w?oD)N3}u0RZbs;5!#)Mn9w4mOKdSdrj@gG4uFQy z8$CEk+QAc8FeMg7dl#hE)6;U?1AQ5S{(cg1)bI>*5^Jo2*@af)s57akLtE)>Xt7aC zeWIlqwzi-I*pI?@1{&V@pK&1wKu7`T0uo>V4!-K$Z5_(#ob(dgn6eq92lH-WB3@~# zGAhf6)QFOnj3K!hc>Vgx@bGWPhl|Jc^q0K6JkrvbYb=|BG+OI5AvirIvw8%Baqz%d z2tP_n!Ldwk=gj+EnlYW;9lSD~YyC?LE0T=Q{E-P}>=qRMy5Wt;-28HM3WtiJiddyw z1i}Retl9MI^a;u2LWj&-5s!ZBU%yu;a+cpG-}#`)gBx`QB4) z*I8X5hd`6UXy?w`)W4)3vI+TrdQC}v`H`C9L>o`p)Y!<#=))2V3n;D2UbyR_S_x|5 zhy^cY)yY(hwGk7=q~y`a!-MNn$|HRe)m6dOIH7G`hjsI9n zVq4`E%wEC9laF<})3mXuXsJFxL!$zqBhp}Ey9aw+@^N`hleI+UzAeY&d>p)=TpxvF zx`pSuCnK~oEpXwJ+n3gP&!4^*cv-Ys>X3}wFPr-L-LhCvS%kRksO?8(%LEhpQAU|R zI|c_m%r&mri*Nh`}mmMKk|g0HiGUko??Sf@a;{mRa2J)#BX)x$Uuh& zO0reVarq9DVe(Sk>(fWOjx!0#!$Yb{(M6jsaF5=+;wXq6=su#pyx^&K-iB{6fUB7_ zr5k3eh;8LwlPc~Ujk$R3A!^}(!p;bJKj`F~l3yoD038}Ne7S>FkSsF%raeEvo|D-m zK)UX>mluB{QDJS6{jRB%g0lIW7dxFxFJE5TQ@_l6zdF3DEsdjn?zrhng)!UV`C>Bg zRHchd<}sCT|J*`Tt4m>|8cW(PnN#K)KQqZ1uo@qeM|`D%;uE-)scg3!H5rtwd3uaM zH;F}GpOH&VrK^hu!J{h*Cn|@j>Kx~(U8Opt!@TMsvWu)H-CrQa>RX#+g}CR~;=;CrEXBBCk-8kMK&lpY*@;@Rt@7(OIETI((2f(%cFG} z)ytfGpEDp7H_Gvg>ga380+z;f3D49i4`1Ldo7!nB&JZP>nl>TecXbinQk9BYb3eI zEae z9;e~EBy(qn5>b_Bj~B`qt?2sIZ2g0BV=1x}Yr{_}5@BNwm%YcbJ;Dd%ohAul{J@Z{ zhiwj%#(24U)>afgjD@a=T`4+JKRCU~rK{x?E-K$+(&mM(T-gK;dCGVitp?@0OMg|5 z8aRZOlw2x3r**C_fiLF#!L&78HtUwV>5~jmk(?~g&C-bKP;2AXkJl8eR_npptY!Q7 z*zG)zZ&jTslFkaJls@5+lV_~I68M8us#l5K%*V+Qc8;Hc>mh-oq59u3#-D@E3MQw za?NI1OBaD*1DBs}X@7Iwa$0;Mwp{s`&@H)CS-?4c#E8LGGb|AHn!^+3t5{1yE8 zB#ej$ogyO1geqV#bjp{+H>lDy;!nBfFs9qZz-Q!JJu581=1Qtw^H_2I7nkCl=)QDX zpLdC`?iud37+wJ&BPCBk2Dm)M!;3AB1`Q88-sSmh38>2Nd*%^)<6Nn0GY zNOgQE)@p}!J+b(@CA1C%$#CNTP;N_20HDYU?sBo$JbGED`UT=~M0&UnLgl>bHtEPi zkv0z6wNqx$JAUF^{=}&_sA@8o{{=4Z;a}k^n?(1bhK#zn6At&zRZU6bNUe2JfsJ8d zQ;Hg6Tf6|-kfEFzdHjU^&~_ZdrCO9lepsYQiK)+zH%ZZO@d!DcM{-e+>vr&K;P=Zxae9cQnMy|6q_2P)l7`tR=NFMm6EHk2juao+)YmwILL@hZdE%$48y?o9yoZaFQ^IUo-t7P*tx7I4b^v;fC+qYi&B=w;%TYnoz2LU9+XCuI~C}hJ6x|c`KcYgNM%;6$<`5VpUxw zoUNuDMA2H?cwE18=bYm5VHeMcU~6#TkmiLk+3Sv5gpc9~NJ1JyR;ubpWiNf@KUzPr z`$=hrzkNAH_xhf-K-+_Y8Y&f&Fk5XL=)I&d-y=UEJ2v6HO*Y;vosnYwmv{sdcb&Mt z7#&`hOv}F&{H;N9w2`WidNU{b=--n*qvr){Z)Z)%7sd~%bU1xc7;d*wxoq^ z`7I9$@0GI1&uyBFH|B|7ybKA`+&ee2^Rie8@U0BP7H;S(A^g2t=itsgee}CU;;Zle z?n}SXHp-Hn4!p+rT%7}@ca^U+-n3X-j{KpVrXDv6QW4V0#qJExc&c_|9v&_X;wV*JgynW%d@PWj;_~FpVwKQ)z>2S+z^pBjtF?lsoXRxSCW-(-ahS*S5t)0 z3gjOh7WRGP?&OMfx4g}Dc}pwRqnX}iin(V-H$Iwh1KWbZLm}*Q1R2{<6pTCV!VT}S zfqdzQerIWRQUZc3BG_|Ea7hwHd%=O~?%*atLy`R%_jCAm9*{B)q z$&O@GHDp)62H)+yye{m?rd~3hQl;RNP4egMj0U=wL2fB`_SxZeHj7@STM_CKVDPYT zs<0VXzT8K)Idka<(zgi^K?j41-V?pJ`!F?BzBCbsi|)fEHMNAeezieEb!B*AYW#>t zatVp+E)s}`lRgV2T@Q|{^i?E#VQF>f^ZU*%WImrzSAKrHw!j5pVn?^KWI_7M$5TrE zI?P8olF)#yz0aQ+FXOiE(aAV#(OB4TlRJd`p`5D>4+>HrqUcEr051idFY}85`o242 ztzS_fB)}R3h64=kqK8kDS`R<(Y$RJ)#U{(CERj4;Q$hPxKc>3-!_y^F4ca=wV%q~3 zA%H*rP;6j!2gaDELQU(+%Db4zl4aO}hX(*K;&VWbp7e|<# zy1bkF>S{RV$^>(9Zv80i*WNk%2GNh*&32*YkLE&Nh}|APlM4Nyb%X^gWV)K6=hU;R5iYO54mNk@?9p~b$ME05hYOI zf(jLkQxAd-c+{sLxFk){4;7dwnIF~T(LiIFnf+(Y*@^2HP(Nw22_kG`&~n+7ltRe* zY>Na?gbgkjr8`=0l_c8j@sS;SlnYCa!m$UPK@2p)eGi1569zoO#lul2UaQjkRa37N zgbk8jwj)U(>~!TAWRxdO=3~lc=y&Psde%sh@?g`eqX+I=pa8ZS3wEm*fqGXT-xv8l zIat@v#d^y9a`-hhhUEJElDA47s-r)18^yl-#V?&PsQn5Pey$~9o=~!HQn&s}ZkrZ! z2XhmSJHpeh7Pen^xA1>w>Ta&Jx{kE9hb&f1**Fl3)>k=D5PB4azd1XoiDyv*F3ZHq z&2SEZnve8G2ksNe3gSW!eDLPbn%;u>Pg|GwLXz*lnof40x7qd%)bTKiRi?|3Z9hMg zl!O-G-l?~lo+Qi!2<-_cuMu_F>A3k^h;VF0ER-!GmUSjZbx|%)0*jMOQz(Vv7{omFL z|GfJn&-$;8*eb8n3*PolyOo2L9f?og4;*XGwgb;Ez6ZbG|JC8%arIs95yt9zWAydF z(Wk2DR}Kq+@H`8HQqRPFm$MniXogoy-`^+W#Gw#vR&NMb7peqG9)3Btvi@!oR5X7j zWbl2egZ|dXGfrB}^gGJ1Cwg+j3~+i*(cBJfbD2*GX-gvBHQqCnmW0SCen)JRbR;8si*0C}cizGKjDk?;cx z5CL1~+L?W)UCK?GP63$U5!|lUm6BV(dHWsiB%4miWU{~AxWs$aN2N?BmYtuLCQ3LB znN6h3nZoogLN&T9nAZ!_;w$Ut)T3@Sn@ziX5Xpna{qi%AF`ct1>~FSTXbmR1dvm@L zc-h$iOLu!P`D8AEk2}WuAlS(9a^j$KNI%P(b?-xhK~>q_^4_DXTScAUG8J%LYzhLO zhT#(q*&p~~3(26H5NyCLgd{QoVFnUM6e|b{iDAE&5dWzm;~MuuC{j@!QK@i>*n3Qc zXzr;-bVZ9vbT5#I+Jb5j8|Zd~AqtG(M*7MKk7_X+Z}5<{*7oGX<3b#Icz&kSMlPl~ ztCncS6f5b?D@WfVO>5wKh!X+%L%I0?>A1(_!97tI7TrjByi`spif8jZjM^9n^fw4= zlSeyhc5iuV^L7gn`h`sFEMt(K=*1{I*oPT>7b%I8pf@3t_;!VQVy^QbW9V(b#PzqD z`eJL62y3@bnVo6fQU^-r7(2Zx?V$Uo4 zxu@5Ppa-i6&zQdMJV~|^SvfT=pSmZ*NXtr#xj$}bysAR=+%&OKN)|KQ=&=r zD@p~_fC57nB0?y12RU%Cf68%~fD%jTY1o}j2VF{?bY*S3V(ADNfZD?Wokn}t%(_W5 zA^$|lxW55OgCUebk+L&1lh;>L7R|$Yp(yc-5uk_nXU1!IuoZw4e;Jy8<&2`MjFr$emtj4o%4nznjWi&q~^`d&P3b zq=D*4YQ&|>5W%bbNF>uNtrd~+VKtDRv!<~<=D=T=So`(k^~_!BM3LrN%#>x2IqKT%%L7=OsL*1JK1VwZ0|4w>=<*zKn>V47o>gnWGE_m1|04`I9nUB*=Ba9+TG&vqNzl^xqJaA86 zys`^?PcR{e!x{|S)y2o*WerC#~ zHLB0ECtYyE{zVd>t|J({TZZLpmkr*-o;9kk%b6uj<9>wD9$X-ZR1LURlLAe2~8gY$vfnATola78XZTUyjc&NQXS>s9f{ksZJj@k zl48JYp&=VBXm3Tl#^i!W9*%jED;4h)Mrnv|vvK@ly;D7Xm)d9ZeQ8VcW-~9Nl}C4C zxoK94)rYzJ#*(lmj9Brbk96WJb`Z;h+WJrZWogahws%6+YV%Em_OTl&-gC}gH3vy} z>J^M)q#kU2ST;4P_O=mGjtHEu&xVyh@qY1!QRNDCpeyoL34Y@z%Kj|O-A+EFsn9cn z?okBNEJc(N-^m#Y6n>c;d^2vq`b_5byr`d=lj2#(A@$OAH0CU1jzRKj_x`Oq%*?^J z^JDeY=;L+eBakKH^Tr>n|scd#zBAWjA+qAtlziD^??radCMMK_2lTC^i z&t}bkl!NSv&Gv!YzhZ_Fc=X34(a`iFM|{w$KZ8peTJdZY1)nYv+eNSFAx&83Da#Ob zRPUux=N6ogXHG+9dKXhkoq|~93A^f}hQ{-ww z(QZp7>U6g%T+

lEhxVRet>skPwL07M&;Arswb79*+};Cm}T+x_jxW{s!!!V)Ii zd)^lY4kT~dlHIDkxE^PLaB%hG&W{xjC@ypi0F7vu+dnlxp5CTCYsofqMC z)2p>lm1`O?r*ZhHN&r%iMh@fy;L7KF*oc#ELn24*HsWyb^3is&q#TgX9xlk<{yNaj zNmtSK^O}6y0Eh}r{x%P=jSE-cxgmo>Wh;J9GGdW{7Hp@U+ZS8FT&(2$gtj-fLGJ|( z7!M5K5Sxo8+WCfZAso}`>{PWD#tJ?An~JZHKLGi3r{4Kpn0odiEGqWS6A(nA2sRNF z1p=U^NRd6hX)YhX<)MKh3fklfY3X7FBUH$=cL;*TLYm~mfIz~#L5lcTP#6RR!1Uxt z-lO~$vQ}8;pOaX5_clU#oQEu$f8WXCks;N{R)l^NB!9M4M<2WOd%WY?d09+HXZg6S z40jZV5CyeoG;LzYD^(CuZ`ImN$NQ#FZvWK^4d7Cv;6SNYhe(T+CX}Efsw76G5!$4v z6iW(_t7Zmsm|J{2#(p2yh{1&anG-Ol7);T1ZY6-UAYh}RTO-NlKpAHPaMo-nz?_W( zNM_RlX4#tgt29x7C=CfJJsWTBK8?TKFfG~F;~XYCX&UQwx@-#cZ8{H7V=-;;VKzxN ziyDnjD@Q}LO+smQmi^;xQ*9fYSYo_1wX{w^Zfh#0+%C>Dp3MHC?GM;F-LGZV#)&w{-aN2RWx@t2w(^$6}C9St}(m|SeV z70w-w|P<44?<^llZgLdqNTn%d_^!#&ZIhNbP z8EidmxL}jh5`dzSAmO&R&d17Vv)vYq$5YG&=KLyeLIyL9i;A>IV4+3vK$d72;6Mkg zqaI*O4vsl7PLeu-)$nn^HiX+?8!6TlUm7MV~|g69|hsST5OR&86_ae+G!KcXVqhs6sr)F+jgt0(o)IJH1fV~d1=-1^`i2w zaUc;FM`I`E?V_ZyjZ>rbqCl61CaoxD)frnE z)|De$RrX=OlG}SkSoTd5X3L zMd9GU3JTrGw{e)(`fa~)f0iG%**H&-cJgUMk4d$y3k25YMO__+0s!oP=`30Wt+zkFrAGliowvFW(vG=fjm&hxn=w|V_%Y)*doiYFHqXhc?991NR7YsAGWdiZo8J5i>iN-MRLd{r+mLbYE;G&2bv13!N)KpR z3aRkr8O@I$!JMQ=#r+!{XqSCI3lCXVJQiN(kvo?)pHo*eF7KFNI^NBcCb_@?QpVfK zN)ZKu&LAWy1Mr7(KWf5J*b+R-ZJk{R)$UeL5j9RyiXP57+po;p$;D!|Vy5u7pqL0` zx1KN*0e;Ja!vRJL^Km#Q$^EdsCdR>nMBDh`#i}IIG7)}0$R*g?@8h{u5H9qs#4WPu zLyK7@bcfj^KrH|Mp2Q@ zfsQOn=&MbMj=LqP)i?`Z2_iRqsqt#%`*&+h$r%*@*nIeTaI+>AMLE;7m%(&=9P2wD0KbOUD9C63t-KS|W){Wwxxv!Oa;C&S>h&d0xJLhQ~ zj#({xpZ%juSJtJPfmF>FC6%BOdoL~lw=V2-1Uy!+rjF>dBHdfX>b+fcDsq<el^IqS2%NBi^?t7kz;G z%d>`ggS6Au%H_3!p9b$jo6{b@h}(TQwvOoP>wUD+{ke&Ugk_^|Scmx6#AyeMEuIzQ z-A4O~M%^FK#-E*-g@#+3_U@RoCCr!kKi^)72uMi@h*kdY2W%J&5@mM#z35=-_n&LJ zYT^YEKEY2qw$*QCLn&-vsVE=W+?Nb^G#na*2vJ}t5zlr*DU0<-&@FYdOg=xknV(R? z3ML7K#Urm|0iir`o@<-6->7iOw?l19;Zy5qzTz$d_c`yE8ymhW{k>`_vQ1+mc$7@K zjrz?M&Mv2Bm{&&D=V$L@q#{fAAON|}o6AsthuV5UyHWYqpUB0)ot4E5c3)v#d@0wv z*3>yC=0qCL`k}DO7>>bGy~Wjoq7K>q6y%W30_JA5WwQI|JoLKCxfBcla4qrQYN)6{ zb^s5J1JXrc|Io$9LW^ZfBj06Lz&Xf4X*SrvBb$3sx^53LxojUr7}_$SD8P6yG&g{4 z_?8Bs`2pruc^+L@>iVNND~vu6Hnu@{`qi{`>1pTe&x(LBTN^XVykyhiGy~>b^QoW5 zmcd7f+6EfCyI!ytWNN{SIU_%|#&_;xY*S>W%ebjNthYq`EXrLeKOqr?S53!zj+(b~ z1_cjJEO(TbIX~=t%C}T^88V=1OnO-w!I8^XAzXJi<2xNxau}FnsO#3IhFsFN z?l;|bAUH8Z_SB^1Gk{rekT$tf8F)F|t`WFJ_U`m9_2Lv`2{<(f1^3zClcz(3lR=~_ zv!CT>4c}*8p0)TseC8Za#!Ad!jrsi;(}G#Ll%xD7*KXV{{`Ue67<*X4e2h)3v{Xft zfsLY96Dm2=Ny3)uNlZ*bh=pIBikD3W3`N4UvawOYsCjxA;NC%8_`HXjl#c8f2Pt0# zdD{9W?`svc##c;n;xVkmv&9hKwNOrM~V`d1EX73u}Q6r&hl)_O7E)O z)cE`h$Kn?^>bs{NAkJ@baEda^e)-hQ^4;(0`j0_ms72=UV-M_K)??j_U?4YiDi%m4 zltxZPozx(N3{*U^aY4$1IK`F8G4=i`uORid2AX@{KKey*RnGT*P$y^&^UXKLeD=od zpBL4ghU;K3l_mK|JQ6AX$D-=6&L=2*lBBa45)2a zpGFeM}5Z5=-o$nVYL9MN?2f6?|9L1U%h zZSdsFV^{tDq%Fo5+13(@c$&xCE|8((rCSE8)|%pjKJ%&z*4MXDP;!U@m@g6>fu1n) z(xjRFAUobv4+rp!ZE2%@YL@L=p-#aB4yNLND5o#E4L~vw!|snXB#o7&tua*7syyv-Z#L_Oo(%FlO}bFH#lQ7{HRzn2P;C2+A1m2o+f@dw{W`OLgMdw9HR5- z{KLd!i8T(Yub7Stzi-#;t983qONCp)y3Ns=PXx|I7;Gwv44MBc$^k%U$qDo!@&y>M znIuP$f%0|lWV2v5#9PqPv4XoIf6`?Wq0AzF2rwy<&_;6^W(yIfK7K%#jR11{9Jh7^ zk8;F?$6uB^c4UKt&T zTr|3cy9;?ay1x2QgU>J~y#7JeDG>3_Z&9S$_ayS>WVT)Br8A4}N^Imv5f8+s0vB9; z$Py88gd=SOj>_aWdqk9m)xgb$08>=VTPRvQ>1ZV*m4vrxpKbUerngt~u4V`8bS{k_ z@G!&4xG0jMb;?CB7dzpJ19B4Qk2(*|_9tNgcq>7@O6{it;Ti}R8=8nui@;Bu_sUTK z5H>O~$R>0W4$-#ai{1>9QD%8IC2aIet1oqg{yU$wc&rWmQ^5e*>BIVoZ(Jo`s?91s>F0@x40H_sx5HTaf0>-E*n;*L5<@d@C2;2aCDg z=K}usJz*aCl%Qfj<1qua?D<|iCNs45`nm(-82Bw6)0p<8nTCT?nB9e@WpZSk+!s4HkQqKvk8O@$(%yKn<4jG@;WJ6J8>wtw7zl!th|dd~iCnVE0Bisg^jbgwUr zvlen1Prbbx-mCDgc6jxP zTh@(CUs_##=MR6>yo?@cAqx4pob9>%i5uGpb62`mCSdY9X9eI&%`$Svt1 zlSc*Sh57;KaeglV)K9D|8b2?g@iFoDxX2ADnDms;U!mWLcnY4-(_>)OcZ%Q%D#!|^ zE-MB~ySa1)G`MeTjy-CAjl+kv-%H3Dlp8v}$ImOYM7gb1;Qhytt=kpd|K>yjblu?@bE$qA3+ZNSsy zXuwmGd{8h7C8H=2_O3?>B|r>pNxV`~)qB}F);g%E)ZGs{4hj+{VOJ2BiF^m8)?0jL z>MSsr$@7lXXCP0T`*;vHpGtv=QDOeznC~PEtJuC5HGg)QD`shw{iAMSom9Jm*uOE^ z&bxy9Rj)rdsLZ+W_P6^(h0_NXJRE(hj#~NEWiC@>NUxDE}ma*1B&Aghhu)$VXkX-Sus|50tJl!mA(7dnU(?olE)EFCD7wi1r0!|F<`|4NB~N3Y_djypvCql0(ssdV_p+;^_c=dT8-rl(V$t<*NW^v z=E4~R{3PY2Sh!A-B$mneL2$enV-|Rabe-IM<>{;q;jw!xvvsC(2?kRr1 z_?lIG@bN%n=g5){j>pRTUvI(_HQoJxUM@F%Oe&C0dTY8yOW&Vg&zJR*4qYK$uBhRZ zy(X1gWf}4q=Xa%G*sW9;$~B<%4W0HV;)f{CXCeX|Dne3J3^-q2k9BrcI0L)~b|65c zy+JPY=~KqgPqS2OF(%}625IozeqO+}@g2RbV6?N2>4>f6V1q7{VXlTxTCSdRPv*@X zyidjg(76_m-mxGDL-laLD8SVo1;!%VLf1dAnb)Tz)&#JzwvMquScFF1%+++zB0tE- zFL*?rb%b1GMVWV?^9~-qNiM>r&)J;~5z?+!_+&M|Cf@KQB#1ovsLPNtZh}o{4@o7h zXB{A98#Qf|FWSc87-X;5ye|53l7P0g*&a6FZ~EP8fBlJC)je$M$R;kpe5qJg`8-b~ijT)k&NY92y}0?2e0~GkC+Uxo3lQ1`ut#N7_a6njSZ)Su zd2<_3e7R(=ZJ#QLCv%kReedu^RFDEI5{dZLV}yN-@Kqk5!t3%*k3+S7-CmV$PI|xj zx|{9%^OdWDXI0LdS80JM*lP+o?ymY@R%uhrNO?c_Bt(W|4o)xMe{U?q%>I*S|GH~{ zK;I*OqT9*PXsC?`7zf*?*+!QFM~8dVn?bCf)uz%$9A_J=ATrG7Q@jB6Tn0*zonQ z+hjB~ri7h&f<5GQ?+&5GqsDHZsN2jR1%bHheU3|wkZL&P(U7j!`m#e4i24daBTl3C z4o8a?r~5)~0V)kV;^oOW3oZUwhK}^_>T>)0N_uq%cr)2;T#2{tiV@LEmX1Gk6Yb@Q z9gSmOaFzTUY`S6$ zA({Xx^A-r@W>aXtEeO=wFyQoKA(1v@IQ@x{2m&&Du&*6L90Zz_hk?MB2>`*bSl$>w zzNlC!3xBjvCSK4}xnH$nT1(tARk%1QJF(?kBQgF%VfvT68CAoS_sy0~(-y^yM-GW} zXt>6-KcCHrN83M%+Qs}6 z&cy)zU0+yEroJ8fag9miSB&hc*Law$EI!w=R@E0k{{DRzh^;`i2r)V%93V!` ziwG+T5YpLRfx44H?`d?e3q_#GSi9Gr9nMy=fSDfi7^^It3OllY|D^3@M7S$e)maeR z)0b<#^G2E!t?%vAqI3Q9>|S-$xEQd%HcnY_|tVgF+w_Tci88BXLph z&|G|!0+$UXRd&d2`v&fqNF)HK(SG#dfo=IfX+t5`52Bii(Ae?CCFwiqBb;r=zx?M} zE2zpsNjrl*YDq=IMHUCbf@z)8Ty7tmz}DOd^POLD-4$72D`^B<(BS9evchs*bkpR% zS*MRS{)Qo_qe#eW7gVs@ti#Qm#k~DU;O5#{ca0+*aRAfUrQO?UgrPgxf2>@AB2Ll*qn>3w+p>6Pb)ohH-Cc+%A}%&Dcxaz@F1Q05Yk@t;-r zH8~|1``y9SVKI$CJO6Ap79AKrs=*Piahnbw7ikaRke4!oZGH|#7@FeXfRq;e60I%#ys~HPK9+;qJ4~q$elMdp@w(z*C_n)?*Vm+rsjh@OkciJr%0+1|~(rs-beY z`HD)({D$(i$iSJ7X1T(r3GmERy1(Hq=!c4w&w5b(X}^8V_VGq5s-|*1ZQ@yK^g4wh z2eTc;J*cxDkCDoO=$Zonh{ra1?Q_ib@Y*1B?zBsL`w#|JCLVX3yadWB=ox@jv(#Q|EQFa%+B-dhziz};Z+ z&k_;>(5SmSpqpRQhEP`d%#G!_MR3Sa&Y~rnIF#!VHJ|vf(=FG4%=xfo_!_P&!Btvo zYAB*J!sf%H3$bS!$LZz~YyC~jrAnPUN^nGl8j5l-w0w0?$&WU?PUbfg?CH=z>SOk^ z50W zt?H_i=$~ZZ_F$DjR>r{9~$z2qJVbGtWA;aXlX@VTMd^fNe4hBVP^>Kfkn(=gn@4xiae{;~KRU?*0`& zgGXik(ZPHLR5){RK#cW;E0JSF*n4jNI zOfVLmmu+8>(*QxP=Z839UWhINOanuKBJ1%Jv4o;f$~mZ4LvMZarqC0du>mtm7{OTtlF*oRBMBO`WrHmne0O zVs`e;XVhW}ux;O}L1Ec1q2xFU75WO)ucRtIj3KuO567Dd)2g0n>-x?NOa?J12IDh7 zO-(qS-IRUB#EZ)s+#TGt@BMUUH;rO0p>33}J(wi4jjDw0<+L)#y9R!IE{3m4Tlu+8 z`=p^}AD{_42aXhXy_>^=yu+VcQ!j6(hrSiwh;q06P z*kaOy?sXzR^*4j9Toc`ye0PX+WM{hcZzk4K=4vgf2hYpouL>M- zpxspZ!W(!Lzg+0v3cH4oE*?^-(bwR1i?mAmc`C)QfB)7;?u{_e{<1J|eJQIu|D@5h z!A4lUaN-2BhrxJbEMRq281sKlv;VrXtE{9=!anxAo-4l7C|Zw41X0X=GOsy`kNEjO z2|sZ3)Q<{E!?3(oKR8{d?1s)3K>9MeCGquvu zaScB|2?>P0tJuF+5`nt;A2H=;d-1~Y4`;pW%0;71Bl$i*lbjFkf7-9E$Km&J3ek2v zX;Ej7s>o8>@S}5H@f=vDDI4jzY6{En>6UmW7(L_3q^i09>q*VT)#^$3X;|L=nNt{{ zdNe?miUWotp%KU=G>S5bUa@-6yh3xXSGM~B3sxl1t*Ha0lxQFMNjXZai0&Q4$G`F9 zyYU!tc<-3qp@{%vS9sj`IwN+)@kWj*f-4-!{TpRsH&S{Je+ewonFO5gK zJ^92-Z@g~mxa)`eX;T43`a*Mbcb(NQS%5`(hAIcRn?xP^CGQ<0_wx(vA6?Z%Z za~QdI@pRZeMd+uNgpJl{o>k|Rm(o_nn1F$4oHRAGCOd@F!?r$dUQy-+%pTS%ON{^A zmCg4h%Nq-WI4NbNcg_aaA!Lum;tzF;S+wvXu%w%AR&$S5RWVDL^O=8=>_4vF{u^;x zFvGd~=@c1XxR*y$x6P>&??E8Tzyrcor%r<7WAiBhqx?TDdFI3+sYy>B zB>9_Y&ygDLkS6z?&Fcviu$~;&z{Xm>o6_FYZ zt~tw`;e$eO{`$}CR9>NeL$j(}3aOiK&j4(a_R1cGGjhB}qM7Uw?hLmpVQhaw0NEP3 zp@bt)ElQf5TR|gCiI1Bwh)Tzg*ih~yr;n1KLitdHmb3c&#@3R2oi}gi7;NmqW2+x; z9X=ptkcU}s!t|&MZhn#ak(W+haX(mv=)QOeX5#|Wey8U8dl~;LrvcfK15%P;AyOX? zq+~T%;4(k%{30MQg8N+O^g_0=95Cg+}AdP)F8 z$d5?ov&6azL0klK6R0+>4`@B_On)QjKf7vw*hBfUFYxpDPAiLoFbikHAu)D~p!X5{ z@o|gc=(|tMy^kM=S&F=_=O5i3FEfzwjee8mk{fRMad+up=c?;T*tk25rH)N1&IIs8 zR^xV5#Nhyu&h$=x_=P7GipkakMJQa*k>Y&bGweh*ExMv4`-CVQ$3e)HvBe``DX z<+I+otq?SIbnb?k!Z;}k6C^wx@ITcHu=ND_xgECzYaZmqLxGHYsayervEFPB{V4K!Q0po|Jjn>7ubX+tpv1 zWlo0Y2rnfdAOK07=yUkrGUf&0@j9e&q;vI~*MAl2uJ_|K;&`eopcwBra%=KxQ)1!u z$ef(Ep=?RXR8Lr8?!|x>_KOvf{KRk(1D1Qmt7)26XCvKR1%0=4&JyPycSptXFPopK z_3Asn_B1ay*ah2{B;-qT(ya)9&wlt($VOjVY1ZQCr_s{hn zZ~*|H0;YPJ{He>9`Pf6blO`Tf&rMnB-d7|{1zqunXbxS@5aD|!!Nj$dH4=xn_fMq^ ziIT#nFdA}|`eFUn80THgoYg-m_rI`gusH1Fu;v;v*>qn#WOoF`83K6G^T=PFU@#hO z>ynfd>O7U(0~EidK@;PbC43ZGVe}{ev_aby_I|*C0QTb>DnACB8f3(HmsmwD&g#c8 z*;})Ha;DKr>iYG!0-zI3z1Wk4Bm{@8aGM(ei!GU`zS{?t1P&r5K~Zf*$ z&L!&Ej3CTjxLc5wIM&whh#)Z?Xp;z)^Ifb2(N+fHQtwj$c=+i_zv3m4?&^4b`3XMI zc>3FgtTdLw#!fRpxQBhB!QLZHs~|YFqfyk*__fP>s|~uxjE~|&lB9jRXpTtsR|E4f zv&B_)UlyDCFYdg47HE`TvCcD{OQxnkgQewI%L0OB-t6~G!MK12ng$CBLVoS~Qa>t! z{EbFIp;#yc1GWkZK5P>Sa>7RH)8H2&MU3^2%Y{MQR@ANoky>O^x55kO#+}^?h-^ha zF0HXI&m_h=8k~qXyda6=ITdaukJ4yWjutYlGFfg-)%IcOE!vjuOA{fa5W5@7>Tq8P zB2Cv|{xa1Pz1jOcpp6H6f4rnbh%-J@?TchYw*g#O%-b$rz3a!>bg&8WspVw$|KRK` z!=eDccHg0A=2{i{{#Ro7!&_ zL$JbrQ$vSYr??U1uC_7h+a+cQ#cxT&>`UZqHq&0U!-m~wWU-$SnPioo0;_YRW zei4MFmyd*&PquAy-s}6-Su!2U`KjaQ>Mb4D=kjqnjTXRaO&vY}hvUP94O0=8faCAg zjzbD|8@V*+?{kdHc&tYdPHC0H&Q=0JLp#N?Rvxaa;&=s6cmB=7?aLeDJI=F5WBE6T zC!KHzfFb#q!67u88T&beW8;w%<7JR?P&gV;PSKI;VPptW00&X_#);68!pUVCc&r%6 z7;$`pG!zCgOzggxBhk0Y{jXj%-( zP@~+F6B*J=nZG$qMWH!?Mk-!SK$wQQJ{@)hh(NRWC_DyIC0|<3U(a##Ij2@X!BYSF z&3yD(qwY%kBDbT#CXyWX^~~jk9mLF3T7+u&G}dVQ7Rf4Rv&<){=(r#2L_|o_#yrkBZl7TG4tZ`fUs-Y|5 zw6QbRBEyg?CXnbvqKKg~o3-~fK}JRJB*G}E&2ySXd7E)`eixQzWGD4rGrRW3R=Lem z3jaoXP@N48lr8nXI-2_Njie(FvqGz5?vEqYV;hu8#wj;5B0;3Z!T#rsHRGZ;t+i}f z-dBAO;@>3sXd^HwYe-)ah-@p$6N~DEUYC`D~+@B23j?N!99^`ND-~h%$ zJro8HFK!M%M1WKPzI=ubGHbfgFhvh)W%vO`xRg&n0$bBVV{{Zqq$VB)LnyPNJC zQ*&Gd*(uZ*@~_SO8ToS_^|c`)lnBU5>6_-4YW0{@8I~Z}ng*`5+JCiWah-l$<JyndnnJ|>P^#v z+&2kmeLD}nUyyhjB(1vTT>gP$$|bZGqM*;obcDqG{oI?5?i^+W>Xx+leqNSvvi!KN z|Nke-{Vzf9zj@-vpx@oML<7hbhKM{_XMnUC1tvHpY~y_V2&gO~hHg_(q>2 zmOfS~xIWoyYj{qB8$}yi%?mLl$Z_nRw7yvTBW_fY?FlHt*J+9Nt0aKT(bGnT~k+{!3umLVsR|Djxf890xv&zs+J+4XDVGC;M zxRrGhaalaRG}~CUI=aaaa8>wl{P1$Fy;DT}Pr}K2l(jhlp;voPO%|&hq%^_aw@#Dy zpGG7U6CZQ(X5Vz4T{@wl;hvVY-$mET;K8aDHqrNCP4qpQlh!OWLuo&FvRJ*kb9pch z<~>hIOtgb4pXTmDZcgsoo)g0L1rPM|Z~%mX8Vf}V1--8rI4H}(6pk}wiwm&D5Br7T zSs-j`NJQN?4|}ahWqH$zTvxq}h-doasKH&AACJDXo zuV)V=xnbuun@b@iWpOMNX0Y-P49-8%h<$>R7HVM5cZ8mAcAX;ZdLa?gRBdTw@hn-{ z0`~=(Kl(xC`6B0H=FTWEe8ng?aHp(n!4moQH;e0!PmfAmPb)nUqd6Z}4nH*Z*zeYz zQU-iUnI^UJKu3f;b}*>W=Ja_`Ib;+=(3Fs&x@6=$#oK*e2BDL5Cv2?U@=fGBN2s^mbL^XRfJZd%>D=Fj?634BEBp>L}ESf z&Mq4cIvThgJ1{P}#%eWo_OM>Gs7%OD+1*UyaE@WN@rw0NBdA%UO4+wyxd5dXs#h2J zfnV6E9}vo5c+!ms?X06Id9IL(7xmx9scDCG&1CqNIb}8he0m0sUt{}uI$JO$xl6954lv5OB!t{rQD=ZAI_j zhxm|$H^l5ZCaSt9?Eg}{>w^FY zoqMy|6uw75KuoOkng9%v$9E!5#Pf@1b%ST#Lf9GWPlg>O!|iw<_gLz-4%1xMV|#a^ zFt2^w9*|=&0dBu~?}n7-a)(V3i}V0;mSo$KQLE7bS*b!X7EJNx-HFX`5ELVFpr5SA z5{=t;dXw7&U;5ZhJ_5Q>9t+AcosSy)b(KfwEPtU=B|Jf!H(p5_hHQtok3962Y2eS1 zJj;dOR3#S!9lg9rI%6CcqsD=O814cwZ~HYTLP3}@by}N)^~dM&Q&7~Mj73x<#n<=1 z^OgWQ4o&-4O1!EAq6LUAh`vD(aB)}MmcW&Q#@;rL7Tf4ZesUXFpUtTEsi}-Js@7Gk zfRxj1=^t`upgjQEvF@;Qjoh|4;P(SC0GSFWm+DN+`;a(%m!E7?&wD$=srB~f{Q)sK z`mx_x3#hZCxF@RBqiW#n`Xstq0I$4C^ z>C-|5`BPPWP)#r22KEgzFd`k$NrMxh5Jy&{3=0me5_8G;qQ7~?^ybnNTC@A5zcGER z(i*!RZE<;h<_ozmhrhD#$Lu9hn~BQ(QHFYrfj3ngciwjq7P{wJI4G+$iv*_tQr7tr zCiJPCnX_stq{kl2wgWd@0Nct&oJ<$Tkx zS^Fkv07r&X)4oK6>7vRYHl>`bM|r(w3<%@N?P$qW7|GRkG9%e2NX;{p<+2Wn(vSzn z=~3J!^8L3anHiBwvM|^v`E?Jj#}@MqKhbO036vxjUN#Z#2L2tsn3?s;ProEAWb{t~ ze3yG6szXXgI9{qCSK!7#ICx!#{PYHL@s_uJnV*LL#B+P|lAV}}-lmK`1Mcxu_L4o9 znq5>wmpL7OK;O0?&x95y{-Dj!o3MlB(@Edwn^ECU@7_60qlS~}8y|}9MhY4`4z|FM zbNm=mx*H?)sW{pT?M?-2#O1WOGKRcZJQlI7uFBq8y+NrPvXjDBwY^ZlV=j!Wm}Pqj zqBbmDr;Gh3m%|Nyp0Td!DmcgSUrIuN>v$Aki0l4iU3U@)j8ei+YNaQ*!(DU8r2@d` zJAZn}Vr<$^4WtA8vCBG`x9n~(MQK5($OvQl7f=@=1g6ST|G(X1X&K% zUmspZd^MZK;X)N$vl z;bBWmVDg|v`lbaPfKK$!{>|63%2%({h2x}JZUwN`mz`$T zm6e_^=Dap7b3kHGUkXEK55^D3nJO)Le~oRl?1<^*-*$t2F%z=UiUA+!@V5Pac=Vd} zJKKcn?b%|G&b9cF$wZg3w>wc9R+T2NU)yAKO?jX>&A_85HcXWiB1=)L*-&q&`U& z3K^uy3G{+55|amRF6#b##a*S-?v6fj;935*20Lw6r$>E@7%7)z$4=brbtr0^ljMoe z?bnM{vx|R?43nP;$iD2dw0Yb)x&K-}CCIUVGCCjBn{xXv$Z^pA1VrvRDh9dGe$uoC z*5YIlQ+Z~S)rl=RQldf+s2$*cGGBe?V{!*Qp1UGYvZ%ni7oAPsZQ7f}z*TcLRp*2G z)`{A%=vIr@^5JNFpo#(0#{7`Z`KRSx{dn@byym;bE;%R(Bl%FJMl1{d>3>xZ|ILqz z4MgsAc4=&)hKYy6g;_uxo^Zfe(!pU;-?FEil|LpgIn7^iMj|j{76%L#Ms_|0X&5+{ zqY{Qq=+MPz)U`yt$M)rsbLO&R+Vzs^KL+~G7fYDj^BXEA-|KG_`fD7QnCB~^)w0j_&9kTh#j^n&!GNP_avp~5*e)3 zm&o8d+{4hJ(A(9eAqoSxCZ~Wu?T!x;^L-f|Rb7{aVPEa&vzm=5{mnOLeHQA&uXf@3 zVsDnSE8Q4}B#M-lEhXJNZjEv)iXYxYKmPfZc{){*J!e$K-KB2r4axYMI0^34 zK{5w`IY}RB*?5~MEG)@43Gly>%i&_9WV@a=aZ?LP6-Z$thzQZK@lYnsXw_md;%HFR zun5Bj#wI>PKHvv&l3yU#4<&%#dqh=J@Y(cC%^s~YrS))+4e*~W)*izf zIJ%vI{BeiMr5OD8DFzk;wRkH@o!H!=Wcc|E8iBV_%vZkN+-!gGa%L#gHTl@1{^$gAm%{j4u-w;JBUH4Gx&GR_o<2(eiXp;f1|Tcx$HLDl@;y^+{c z%Ft;Hsxa`Y%|EC4fB{Q{R2zAHvbE76-@n`kMGi+!I`lOa3c741lES?Zt=vpnp13lG z>{A~x`6KZA8$}_fcCh3RLvgkGa7iryfX2|Y7V&P8w8z|({A9H~mh3KWt)_=L(f)+> zwtK!5cZrULUt^x>w95B;y^b9Fx5t)=G4)bk+)_4OTHoSSBO7v3oea*RYU)54X%%jw zr>7ga=#L3(a{y|sK`LY(Ic;6Q?{G$3?nZ5y;?526#9}XQQcWHyEDe;LIWRN9PeQs7 z5e0jyV5~q=&JhzmyLjtu z$Ha&aK77(%e}8J{7VUR=Gk?ae$9;R?S^s<~! zeQdNe5Gk>ZvLPp)ioJNjGTsJsc)jQouSi1pixny%=SavKzTNbSTROsE#Wgav>vUaO z=Z}lYL{&8p_i&;v*_=kFzS5kpP5{-T1X=3Vqhw;zP(3a2CIC+9T7TC@p|UyhFUs9y zKBscNx}!lZ)W0<#z-}@y6Yat1lmwwe7{#Yzn(l$&nDku&>+xl1&D&3~a4?cKFSIV)+4o67Yj+tuF{`{ zw{noBSb~7q_&OGizUvIu z(rZF1mrrhRcpHQ4kymT8x2H;)-b?Z|n}>-AAt!t3a0gDpR8|a63C}PRc@I|eVk|(D zCsf+kD|8x4{f$PFmoNUU4bM5j)Fph*2tOkRWrswIIeFsSJwi)a*=f#Z%cVzOZ;m}* z>!ft3jiP>?!WeiO*0AaC;@6dmh>o7piZ>)uVVFJA=I`H@9e@U+hTD~H(DAU`!6h$s z))TR63W2CMKsGIXju&+tBe3_#0Du73ni9fbw9A&8H5|~hxUIykf~?1@xbs8d1&HS${zf3jBP24 zJjqMYzt@9|CdZvQ^HY<|2q8*%2>Ye~qCVq;>kBnA5$3E?7SgFB(NA3=oLpTWGE6Ki zt~A72KGR8le_NexxPJdq!4GZyb{h6s@NX5_V8(lHy18t|f75Ux1A; zl4WE9`Az$u%Qw>IY*XoOFMu{8t-1H=I#o^nj(IK>SJ9$Uxz#xS&OQZ^(DuK2N9}e6 zI=mbbvhycZ?%f*8m%g?gnPtV zX|U3Lv2&bS&9G)`CCv`Qc3?X!p#2kiRD58dd3_C>Ujtt=JWLqddeKVgKJ~Ddr)~G6 zU9z#(uwb#^Iuheyb-PK(y6sZPtw0;!d6&0(NDY!IC|zc(|At7)l@y8O??69zQy2)a z!Kd(;q`Mb#Xz1T$K+T|y0A(LtWC$TQ!X38AW5W2zm9{sO?fCilPFQpGXrv0+^S(iq z%}&}{v3+!3tH!pxd;!8F!yTG^#1X}wz|xqENhI$Q?JI0cBBC_$a$^mRQq%5zp^0br z>Dnw}%hC5u^H^6?W4%QsAJ@w`cDgY%GPWM%!Y>7HQ(wK0pJd((HajKiSupc?BuyaY z5PLIdVfM|>cYBFLr`>wO$JN{8A9AesPY56&pl_nyZdr6t%wt_z3;K~SN8 z*|`&o1gefdM+C>PU1_XVykMKakco&)K~`nUY0NU?NW~@V&`WO~kz@Qh7&&NAMOkU| zaSoiPU=M%J0h6E)*LGp;9*!z`V&5Y)$wP_mM6;4Vi%Pjl%&NYioT~c4wE8gWdU}!n zC>nE8-R|TT;do(<)db3wO?6m`rqhKZ_sO%xpsziY!~?`Qh&we}O_SiVZCYS#U_m)- z+6E4;fNpcPu$as^P?G0bz^rgg#HMzWOnDC2FkWZ8s6_1{wYF2@p+1A8j-x|=#iYp{ zq^1guVb7?Yxs-(S6RJD*!1g?w)X_ zPt6b|sHu*s&<$2S^n7*oemtr7y!*cJTowJW!|Y%GA}Nvny|Lj((y$z$!xgmu_QAE_ z)Z(J1S1DPF)coU25r>wrOUzhnB35#9?Mt$iH@YSN-GlIjMe72zMxZx%=8Lyu=s46gUZh`5nZ2c@86NGeL3E^A7&dQ(Fm&_mS# zAf1djB5avgOCBC*VS+zTykab$S8z{#30AxAS2vYv7oAjCbFkj(eW_O3%D7)Xlel;- zJzI6AV4E1M5SY~WwjjXe+1iWnvpRe17d|KXo#eTJZEbO@H|)9WaC7HyU)&7pG$2f7 zJA&T#!$@?E*V^MtG3s2ouD10>aie;)tf;g!*gl2hSFZ*Cv2K~-`Ec?Oo;ZV!h_!s9 ziBI&R3_~%LA6`M{U|zd5kEN-)Z2yp(3H<>;TQgHQb|EW)u!`aIR=emlm3 zVsk|xCPlP|tvmbH2mnB)aPJV5IvRwJM$MH;!8lOQnywFAk90;78#TS(3XYo>_g-tb zF!W`+JN0S5y?&idIlQj+VI8O!ZgCmsy;Ye?Yph$#2WsS`MB8|BaY*!y8=4vFJ*r?( zeot{y0BTU^vkGWyQXs<2RI_KWB?oEEy zLdD+YN^FH^qGUm6$p$VJrXJrZIInA@$RhCLL!YgBQ7^;I%Klz($GH^R>$lWuZp+o; zS1WpG?R&2JvYgnMkKmi{$J6dCXD161rQPuC2e6=#9dW z<+JZ@9rTFJP(R+6Q@)MGY$}n#Q>`s5~xd> zCc|`!RApiRbaKBM6g{G8Z1l0_FI)!hkMc@%MjPNiGVbGpk+vQDsTI55b}68wd^6N_ zc_|ei!AMxNjvoEhT8&M`!r-pCoI>{AGma&u!r|ouPJIG;4i&Pq7d z_|x6NY>5wJ8Bxr;yA_6pEh9V@qOtwhaAb-Lk17#r9N5g_8*4^31HuZQR?4^N`}RMA zAL2C64KvE+CkKu@8?~J*ENz2&Y*N+Nt=U{mDgGGGU&fG~T~p@D>XT6a-t58&?)E2Z zkc65NF2{XU`+KoisFo?aq%u2P({ePIveOv7{ox0uJBg($fvpa53_jAJ=EiCg=|Up$ zizO@a&ru^~6KO4nwOx{aQrmFy=4TcC?mwcnE8_Ud8<=uj|mwy z9!$q&FsD$d7VEuKm-r|w<1%F;V(D~N&-)pc-JR9=xONMaWZDTCSIe8WFRJQCD4|(TR0Vbk39Ni$m+nn5N8s^h4c_uNCGMg~o1T=JHH)RG_uoi8q@a)i) z$v%>Kl){8P4iFoCDsB>weRSxQxL}NW?h8}92UOUIkXTTou`~j@hvr4H#S_THC?}`@ zpDkJ3A5MZ*!Kuczp) zNY@{G1-#bB?A00l(R2E#v%KpHK+!okWpa&!BuSWLt2P{IbBNd8w}vdNKYhFJEq;A>5<*=1ZZXNLO=cW- zs#eoT3CBi9eL+Wm1NmaU@jJfiAH;ksE&Kh z5e1drhZ>O45$ovc&byKwpZK%&)z`9Zh!hkK_j2#KDbjiGY@?gNIQ`X|vS( zQihuLkKpNHoETDE@O(%KvE3U6Z=W|m&^HY@E8R;Lc;#C0WC5G423o?xGI4@8@tjbT>R=r`=@ zvQmSyNz-y!91ilj(&8q!0*>Y=Ja}nINix_$5y&#YX+@cRB@EgL*7Yc+wY^Ejw)k2% zEQ#hI>d7$1_bLYi&*{ z`26*Tm#ZVAe0aER124SH?mYd6a2*%t#XKnDkEr}v z-W+X8BIO9$#=feryEEY%7X_i?ZaT-TW zCs^PkjtA$S9fMWiFfZg#yIk?ema_T{i&u!naR8fXT+zJpv)*Ez4W`EQVwrap7WqvJ)`69PUH;zuE> zioa@yRTCt`qeH&$qDvf}OzkKBSr1#rYBv#mn)ST&Fpp}Rv(XW{9>~h9d1z2*>d$7( z923Ca8^{RrN*q$sv*f#G9*G_kv@pWQD4C$4iJ&hdX=)9I$?~H>saUNbVuxNonyNABr=FQhcR`!WS>w< zIW(+{kKi8&R^KnoP38nQWkzxL%#AaiTm0nCc~E}%EU^-*5|NRbmOlT%f_tCQY^$A>BIdB}!><3mk%Wo*91 zafLFo{G^}zWBY#d$AXm@08NqPlmDt_Yu}VnSa5m&tOzNFGLsW56i4yHfy`(VDuX1! z_w|uPF(gf;IB#&;j@p>Ww9%8Mk*u1!k<0S7pNU$aVQd89Dr9;91FPPWG}i!SBu?$G zypax%k;yLtb|*sZT&8%a_X6W!m;awg9&y%@(L;1--07w9Je$|WKF?&)yxZ%jsO)an zuR?E(U-nQCz4(V*p2a=@V>wW&xkjTwr0@3w8ab3CGo1dUVeVk#Im(e`Br?VnT$(;E zKbn4g3irwpVwby>fEzP}Z4~76#Mj!bV#=?T&{)bzHU{d6J5Wr5T2GrUyJr`|vaGzr?a- zHnqoxWdfjjMfCm9W>PPs5*zl`oi8|2rhdS0D?#X9OZ1ICD$q?9L%Go(`e`nuVhZ{w zR;qFx#7<&h$ctxiUTeDeNEz1VFd{7)t*B-fM^A|ktd_lJG9j!qHj@(}r*Qc}A1ksQ z|0ZpA|LWAR8aFl2^F2ZG2mKH9)x`@xzder(r3XTt6qD(5ZXB(1qnH@h%v+Z4YTft> zh1_kdQghQ@#4Z7Gw8qjH?vyg=)yb_?fTEeeZG8>3Q=Yma_su;f!~y)5Bxdj9RB} z8;!BuED|;bW0UZ#RG$^GKe}&f6*5-XKCUE_T^Kqp5U!0sAT728T%NKr>$58u_8{+r zGh!Ad-$@;#H$<1@9-D{b5KOZ7@i`LDN@5VK>QXR>9AE$d*5&QtSZp-lG3~{XnH2AcRDw2yvwUfS&N%O6-5>Dw!ImzG3)

N-eYdSPFS4ri3cm(1vIu(>us&$w3W^1J?)RU{27HHJ{8j2;d1zgALgck1HgJTpz3b0N;);eVfw)UQM zb`3bwk}?@(sZnc}aBGkgTgxb7p!gv+9!#AKq0tsfbC^nlU{Y#Km1#(GtUvE3AU7z+ zVoqc3+uust>8qY^ZD?*SjlEsJRG&@dYr;P8GA78$EvxyeDM%FzNf$0A&dymV;!-IjjXB~MKO6eH@lY@9Dp)%tGV@Opo83_xM`WtZwHV#!*e?`U zja2;Sf|mf!F#t#qC^b=U>!D!vyBqDCkhs5?{*>W$!92>251$yql!|9pUeuVLC+KJS zfpj@e_S;^U-+K2MC8J>R?#^REEcb8*q_0+Uiq-pGPK9wHZmcgbG zE{qSN)D#gTgA}ZbL;o!VHzMGhDt=1wj6#bp8UKYaKfze+NAkFXRFm#8vo{P`xT#nH zU&ui3co*a>)Y28gCN9@sn)u>KP7ILGRw7_@T?u$qCThp=)%kG`cESzgxYi>&Y!c?E z#+3W!7Q-;i*m>H`lnI4ktAd|Tbpw<(33REXB63U-DRp}q$}Y47%1~Jkc|BFVuH%t5 zrZhDF(g3Z2i)0+lGdO6)J5o@O7%PfC+AN~eNP0F;_h=D-EkyD z8i;_V3p3FmlOvq$e@S;lfWa9N=+@KPmlH5tTVCwX5&c%Ifvd7+0cMI99o_ZCml}PTcr%myTa@`35?*1G#%^VZ89Qha*|K3>3?^q}$y>aX*Q$}H)f7hyz-D>DA&PI9 zQALeAX(RBmkoZ0}U5}n;Bsu|GBQaLS`#?rkQB&&+1-~47nQwOUAY!1dAe+*hAcib` zZg;HX57xgZ=LUO%12cl8RO{`2%2@qAM`cH*M%%)AcEK5)3oqfvFTqx6x|L_HARKJ6 z)T~hnB-^MIZ*SF^ScDZ!t#5mOA+AJuu2+dD1`X~sHAU((bPGUk!{B46Z1QlZ!FyyG)~)C>-go8t5P-iMb@*XpOUxdcb8%(>EdWtokd(GH-Y+wR&W+! zO3a-YY6cW?1&qvjC}kC40tz|^)t;5^v&7pn)A)#Df~bBKl+n;gS+QP<33P3_Mbk#Z zFZmj>9`t_p3pp)PN~vzIwEY~a>Z;Y9o#?QF^~HTT-NONqMdRPFIO#ijiI5m}-Pf89 z4XtpY9^AMPH|A08tJl;;ey?-RrULkLVY2$R`?5b?sknUi8SsJ~i3bw$j8jZU&b(z6 z3yT&tBIKGKE&lyBX9r2=ldjyep9LG0=vDyJ)aFo;hwb1X?5Lan#XLc}uhHUcDlQ?E z`hCLt6j`1R|KxH6Q$OKoy?VpPE~>LCaljZcsGFVm!4qe9PT%8mlZ3Pjk%y_30!dY= z#8iVoy|W!55f8jqECAQ0+l@z z*&&=~liBErzUT{eHj!A{)eaWCainXer=Emo!g0B$vKLm2v3=T7AEZp3XJ7~|c?F8cPQY?aKWS~L*-<7X#5d5k zvp+6c?L<{AuiK!0?I)ttJShj02*`~U>lP?nQ&zYC^;92yqhq55L$8O>&etO&b-sTtJ5!G^- z&PnGhAAz_4nHo?@YLfuu$Wm4$uZ^e~*w8n_m@$y0pR)jR5PztSWEGL*pqy4MRdYtW8r;vPtTj2V#B1)PW!2mAVuS>7DJ=JMk(f zc!yYiEE{eA@$@jJGV73zm-n97?cHLt%~OSeRnu@DyWHj?Hv{`p*MX!O)YeMTKfZ02 zw}_y3%BRa745}pr8}^-#qTu7osSpr6TH-e$un_XVynM#YSPx0oEUOdEWJ2{$1{Dhq zapa-Rq0O=2l^Q+7uQjN;@eB_Y_6t-*IjGxxx7AO7MwNH$7?pDFLTOdbK}+2{ACL5> zcC^uxmpA68(X%65!e5#fG>vq!WOMBdba>1)>j zZ9*S*g|8UOF5Cnr;K!0!n3rbRVu+^QYEcR!@+F6*!(X1`wKWwZ^x`U`2#qG8;xV1p zm+U`e_oQ7tGXcS0K9(oB@De-xs5dn5a&TPRDXa>=T@9}rrcJ)&oYW+61ZsD-!-@y7QEtLynH z`2?(uS0>Mf*KBcX1b+{tE?V3X>2N#6m@jjdyn4l?xSHEU2OI5JS0LE6C0wVT%g(@x zwHcIi^(fGP-Y;bfHfw0Xo{9O_2SJN?wpxO^7Vu-fd%kym`l$>Z6&<%SN)R0xpG0_I z8#1<^6km`Tw&g&s!!3m=h3$A1tz~o0SEY!#Q}ERK*~wzPY7F>D&7#EEi)%zsCA zN6@g&o1YOC`ukxhlySBo zN&;(^QoECRZ2&MPqD;AJ1sIRlo zMbUcMS6-7E<1*Q1x16*1_osC4$WRHlhC<9#TTgmz61l>7JUV)CTDtDa7Y}y|^xAd1K|rdmojrOFRS1$*yxXrE2>< z$3SwYdgl~GH*TD{syh*_VN}bb=NKixRyn5Rp%5?TCp?YObWhna zDL>|CRccH;Re%*7~HHRY@7NR%@f zRF%Ca095&(W2_=;&pnd%|Iyx81;rIcO%5^y4>P#CLvWWMgAGh@cXxLJ1PJaBAh^4G zg1ft0Ai>=w0Rk-9t=j!}>#yCXec9Uk^gZ0V54XC$Q|EM_ZmR*?IGB%&*lWYeMnMTX zc*gAbGJN~FzzwSq8F^D4ZiH}4>owm?B^dcD90FJ3h~bw^Dqi}u*nvy7=@FnLct(BQ z-M{l;O>3M8S5|Tb_A7z1%nU}>7ub^soy)JIN!Ju)WmQ*0Mmp_EAB>LT?iH=HD}Zv|xk6i1#?I4~`Msw&sxiD;0k(kpf1 z&%0ZBvfYSp?FpKxeXEja4%vH%X9#)8#NC)hA{Y{tjoK1-W@y#oci7v^RgxTX>o#R0 z87g2HlyYGv6BNP2U4~N>y=HV&=bz@AW=rnouk>}3yZkiR80c;8Pw%X~c0cBrKFim8 zCLWFoT8aK%)qQzCBQsLb)z-aCfdBL5g?F?A`sO{tmM{}QL)^mibuY z@^j1AEvQR4@802`K!89t?9}9fffPz< z7qgU@EKD^6RcGgmydQAr}zaco9&|PvQH*D=A$s^0}s*g3$uu1`2JCeHQ zrS4r9w)UA!fwHr{sO{tb>bo>-w+sM!Xb;-j$&U)Yih>c(afsbtS^bWfU)zY(H+YDd zJSe+kv=gCk%)bJAG0>YxN|nyA2+WazU9FE=@CTje&v5UT^oa4;licRG5%onflJ3HUNyb z94(!2z&%G7nC6Eax=}-hxTS|`bMjY%gr^6NLGU@JwQF;@mQOJICYD!@CUZEb;@EO%h~I$ z;-g@Q&uO8H?Bec%!OmgHCz*{^mFbpkJ_6~Zb7->Ex96Ot*Wklo1P84h^IQS~r`bcd z>U-^|VT(}|%mDcted+>;LOYlnb(nos$LzcL5gWYNyW$NU6dwcH2D~qPMnf1E_(}+W z`vr!>5O9<;fTx0&f|tnnCAMfm99ji*R8ABniNS;1F_fN)i9ySpF>{TEOw!s&>L5uy z1t8&uXF=>DD73pXg#L~7i-?>G>!a@x1U^FeeyfOL0$493d}wj&sT|MHNhNqYMxuk~YVb8&fQc6FYH2jfB15H!QD)%@kINqH@PD zP^VkkvxQCY5D*d-WznXjP-Qqo%Yt#2OYNr_aYr^w-zcGdb>WHR2n>Mtkhh-z!+n6s zo_b)>qXQ@gsC~eaowQVZ~<_DJC-a}lj%_p-m*f<%1aGnzR zQAU97mh0vETjs*|plD;LiEfZ3AUn^^0_*cWFq01Q*@7gO?k%De0D~p<9oD2=m@-)E zI7N&)3s(`QX;5M+8?0E>kG(m_7bO!Qu~Sy3l;(2DP%2teKHImbWr>I!Kb{BKWV8~a z&8x&#OlPJ{{(#$!5w?IVtYv`N3IU_ID67$7skDb`3W*{e_a%qIJGE1^?!dlkhumSR z)Y{a~e0R&TsPb1<9$-GiJ^lwd{L_a3WFeoP<2aa;6vdw^;2sUx$T0x@HU#dKV&x_z zi0`jdmpV8&@f3)yX+Tk^8-*lK=Qy&&6xkEv{}itXi>Vh%)>N9_bd^((fr=^{*!k9a zHOCDh&Q2(2GPAIXa4=x|hM5e{65=B4>{tBjLu`J;#KX}h$U1S?VtG*r3+B_QgnW~! z@%veRYFM+R`013ns|=6z`(7nq9}6_P78Hsg$7$@z0N{ocrz12*ZJ-dL#1o<-)2`{; zhG1RkeRFRlM56MEv7@YK-0rW9_zoB@OZ6ZhBx!k>l<7VX<2e&5fhDTle|Sviu8o>>~y_JqB?Hq{Pi%us#z`8n+`bt9RE53RS~ z-G}rD4gRu>&FPTXcs%i8;>Yvm;KYaO@RK`0hFP-u(t^GS>ElsF+rrCltAnhwx!h$` zwT?7uD(MMoUFljV6RF73OGiI4=NN6(VBn2Y$y23&z|;5TgpJdTuPiL%X# z)5P5l??AC)E3@K{NEay72RG`njZA1gfu*w+dRPd{1hyEM6BWFQXt`AAv{of%*Ps$A z9frkv1TBWRyinua^X9hV){p-nha0dK0Q9mNJeDK%;_E)d03K43*&kxOE_ijXAeae* z8-sN<&OhyvhoAdWw|K^}6aJ{W92XENlq?hCp^Y?CZ-@BuDs8>-kfngq2Ge zk{cX3huAd^J*QYARxTDOCJHWGK_39@w?xS;RB{5N#Otv9NalK1VqAf{5D>%e zGc1lh8bqZ93}g%!2U+u|Ig7DShlwp;D%~tHlro1@3v7PVvg+`-U1arVvW+uTEKIlj zv-9)r&!C0qc5Sww7HOOYo0}Tx=`m}V8^&-Y5ZOO|oO~vk(%SF*?K9N1?y(cBw7kPX=<^jG zUMw-4h-8vOPI}ud;xI2Qd}h3Q==K+^ZdS)c66_+USd=3dnOr2Vus(_`C~qX*bV&+p z94RuqSI>-3r}=TOH*n(DZd$5xYR7&L~NQU z>K(yOniWBepx|z$i_fzkUqAOe$}6F+*?1c5|E^JLhXlI-t~Smm)$yX<&Z#n#O|hb& zwqou(OntU!-~5AIe)xR=P+on&)*6x@$%gD_XVIxLvV^^P(totl zVVG7|&(EWZdnSq*iP74yF4f0^_j#|EJvLAVfA4(aaQgl4B93pHsjiG9n&bEFduu0Q zKQHoz5Rl!ZlBzn0&p5qCb@<|Eqwu-is;*brXC_vi`-{g_WZE*5Y^kN7b)_l4U8@{~NaB;Lfq6KhOD z?_o!dc3!;~fOQR<+`&wmyPYc~gEDP(=&L>+(a&exs}0SGq-a8s3-!9lT{hr0O45?g zVCt1Uy276Rex^&Pti61-CT{%6%(tXc3d*%54{jDeosBk4zztF zUv;bEp5rhDDnm?M&TrO*p@#|@V4~m)50(m{PcV(dm`qWw?StC0P^adsI^h(W!@2Ta zI1Xp4FQz?}i7TaI!nK2)pmO*vaamum1~?8H5tzy^kTPQfz#@nlp$QMfG#E;sEcbSG z5~KETF;JuK_#29h?tmN+d1emTX|PF1Wx);*U=yoHko;Y4viocC)c$tx7x{5IoyA?)tqzb0OzzCv ziZg_xir`%I*I|Q`CX|9?p=zke6v>6Nx*&=k8(0Ez2&k`_t{) zmw_4;C?7Em9QxAFZ7UX;r(I>mBL$ZjGrae5kr}3WY z{v)5jon^X^Dv0Nb#1NF1d-GZHieh?KE9f8OV8f%~h?=i+IZCdpXh$uWl0`M>bQf3| z^R`#GeX}VctuDS(8lS#4RjEG2iW-|UOH#U)5%0(1`dJK#{eG<4;e(QaAt~pk7cI`X zrfPiIY=#wtc4ANZz@FK?Q%N0HDA~)RVQoS2dS#_*aiw+Rqk)&-ecCFg(Gm0B?fiDr zd)<;d6WGM9JHMMxO1f{uIrArZHJvscuq4G|Rb~D#?xN>U96Q1G+RwD76}Sy;Y~0f& z4NiNM#gaT91+nPNfSIv~>XKUCpSl(bRXug4(cB=m{BYB<%8tS}H}mDpyevLgk*qV5 zS|7$3{L*>NAm0094Kst);kXaHN@!`^a(yPx1D`c6fq5|$JIV-5i9!hhfs+n0!Z(M) zT-4*Bcb5C&J-_;d#?;hLD>&g`Q|4{<1=1$_n-fB)_^=6&*4kr{(Yd2SYqBrh<9srMh5*6bnnC zLxtnhh1Qq4sx^+zw%H@1I~Noa!udn_nWx189uH%`BFqZ9ZdI8BnQ!W-O}kC~!DWgc zK!bUlz0ggOgmn{o1x$W%(rrB#TvEIg6eiSx+{D5i@PkYDq8BbIJ{;vCx}Of9uo3h( z-|2mX*^YUmb=NT<0ntCh8Ls?MbCF*WKA4e z+k7hVLl-c??vn*kx+L;KjEk8`B2?rGZ06Zp?xph2q_QeyPh0h)@%s~M@G$RMJ&8V} z?Z{B6C&xD$doM`sH84 z%k6UY@B%ubBZdXMO0gs?z0B`phi5dQBaMn#jl-~ra%<=Zg~G}~S1l6GD0U(>aj-8+ zj5aZ;cY8zT6((jJNm@mKN}SN$>T-g)RxxwmDbSZs_18fKhokJL;pr=p@Eg9y=*9?4+907r;^U|mbCGPWoC8z@utSuPOX@-Vh`U3snuX}l>|zjk)-qlqT$U-!rpWg zEk++y!O2kUQkhQ(9HK0M86ft_BZ{$_pmQ$ z4-FAKeGQ1nW`W5u_s z+ja2EPo5zE_I6k~yK#`&&;yk3oT6OOiYHF!pMCjmI)}7b)!99YtxV0|$VXT39oU%l z4YaTI3wCFb92^lLfDx3&!Ht@$Y&u(U1?W3EGA9RO|8YC5*0;_n0yj}cGsN__ zSzT*2H3Z)6F=X?SV* z)Px5JV`$r}BU6}`a=e_B?=Make$LDf<=WpU)LHcGwE2re8uG<;EP`pm?adjKTHwOn z81d0?_y25VMR3j{;6vyKW(?!e<5wu zd?NkcQ{g-V2Z+_1nRsS!Gm-|)|3&w--Q&>0L01}2x&lpzW|&S z$Z$0Xs7!i}-@5J1-^fSAy6c5^hjMgE-li7QO)W{o+^c8a$`2!1B;pc-Fnwk>V?48=5^ zt8UkDx#ILuXm6fR{lH8ij&6X$`tTeYE1qEZJ~e-rMphgrw`J|i(ewnHm>l{{Ia5l6G!%mFQJ29b3ldcbDDtALN~kiIb51b*M26OTAhDDV zn$&xIY9zMV-;t|+M}!-DOBf>_Oy4-2?E4F15V~Wyf{T%~T`$pgKV$2(pHq1r> zCVTUs%VHOdx~az*Hgf-Y7NhJF2jfA6Ys_f4s#xGZ!b`z)djxE9f;t?Mpi+0GLjBTs=zr;rnl`g3YgAorHs_KQG44fh zSjJ|S>JoQ=OotN9Iw$rki4jAU(l|U<1q(|%9@o!n9OrI2H}G z4HmErai2|dccQZle8VaND57dzud2&m%9~iOWAw1L*Rm`@2Vw1$c~&}J4|)IC5Lr)o z+deVujJ_XFf}b@3T;gAm{L{TAOqkalp(@3I9>8qnn zkjYL*gJrb{zWsb>uUa5F8u_j=vNj!j>usa9BHtHb$bcEC|~ze{v!~AE!$$lneQI^ z;#`eqtZrwRL?T$_EwcaFUPVITmv#oRQO7UuDI=b5Vo4dPi)AY<$IRy&eOG>7?OkJr zNsRkSO{hFlm@IP)raYe{1grck=23HfD>~_dANfVcl}LSxdxjSfJjEm!q$`nQBIjmN zZv+4sYXkC?BUWwK6oEqy_D9SsTuC~bq3^p+51pv+n-snd*M}b_FV19;G){Tb1rgRPPKCp8)-%tiY2FB}TNoqJ z@EMso!a-XW;Ma*k{Pnl0Vut^lnsoT^Gg2BC8+URqbazm4%e)L1MTYJTG!`gxj#hfD6U%GIO30nx-X4Q-w@MX$XLquae_Ms*$>#i%1%%#~JeTkFju09CWNf{1^*DDpK zEC%07pn-#qu?%EF#grz5v!O}Tc|&@LN*~W*7x2Sb@;UvnckQNNhi8he7Z9)8x@H$c zrK#-r%CqTutmtj*g+B{h9i@?$WtSM!HIB)9qi7HlrjgH1h{Q_$=9m6d?431L>QpF1 z+i;vR8EU=q75S9*ouGG|IA^z~6&*1Gd;aIh#4tp$-isw=1ZFA%DRvg6X!e-_bVgcC z0$eV%SI4LOFnTl+(n?4;m2w|EUQbd0IIlsd{Zr=%w&PiB3i-IO1~?vxj&ImAsmdUOV3cZDbZy1T|nG;8g2FG?H@ z_uLpK?w)Wpt9+zsd<5D47;Vc0c*loJ(gUK1DM=OoKdz*^KgH&^0yI35(uOxv%p1&c*}%l`CwZz3T@ntb!#V#Hn)*|(Dd#+1eb%W& zU>_Jk7rmq;Si1=K6n)}|u>IE|d$Bid%MbF6_VK!~Rg_xiQLN0WEAg#Nl-_(5rl) zN-Re0`@vv(MY`#JQ@ueSWd_cgjF34eV6RG^vy6AJo5Atq1xuA!^OpY-L|BUC_9U0t zTguA#t{?~+xTu1&M0B@F`)(uT@Zwhq$$2xj9k1pmn2BKS?SA*Cy5aOX74}wlq|$f& zu~@pR_xe?~xG}m58Q<9vy&Z13t@n-6FSc=bv?eB7y)Dly^}kQcVHiEhVOZit{#nZ` zKX-9cK*&>cY;_WDOP6Awoc?@xJEr=Yt5u)y;Ilrq#P{g*IH^TACg0=8(C%L^%J?`d z)l9V=TUJQk_0nfNgP?&kG8?M4hY%|=#0UHSBbCpqO5r4^et7KR<)r=N-Z(FN5#2UE zdRX~Fb9J)94z}Kz+{7dr&7AUZWML(tbCBfuTe=AEO(w4ylLx30yEyHKe5bmGC~XVH zLKUl7t5hcl9if^i6eBXKw1tQr6LYw3HZV7DK3!CaE_|7U$pt4cUXK?V?gc0`OIb6M zL!hFuB9%QDYDr2ZMd1$H!mSuGhnz|Xd@?)gEtspIQ$96I*tM7VS65xIUr>O4K3&Yz zW*7df1XaQoIcg#Y&*MOj(Et&On@oaKyY(v*!m_%$ng`cNyHBw{Hk>1jzdwsM+Rrt9 zb2txOOa=}qUWlZyD_uotYsx0j=ro7tNZXmvu%r}^;wW1t3S<5~1G99Mq{`FEsH)z` zB(%J9%FBI3b7DzRi2vgvS2)VU9Kb1$goCT1EQ{^@)doUI?{kOY{`2ra5%Z{X39`vl9`i~z;L*+vZ(YWk^4&=GS5achm?M?Hlu96 zEM_mWSWG%a9BQyV58RtkF#tF{VReOuJ4FbbHJFXCUJ*~YsO2DkdD(;ldxxqf{%`?jV1K3^U6hgNdpIf&7}2YA6~f^_q&l zTS2p*Ui6f|`Fu`;qhnYia5J#uX?Yv+s_>7c+pJ+SW$@`Vc&)$qG;BCsCw87Auiij>Zy+V>YH%hSYGRllZ@G zssi96om+^l@=(6L=APoyjHA>nprnC3Znn}!d$lA8YVJM1- z2q@h}XHA=5j3;NaF??*hhmm_WsG8;#p%$)#H1MfNt#d52A;e_6gkg?F&AN8N_i<8N z;Sm4kGC8QJX3FWu*o@MG0$m;VI2ua2`1)=D0J}>T0W2TsxUMy(+Qq{uJv;{YAXYTn* z_gUx=C8M-DViD3=h&@%I^^x>QhRu}>@&H3N7{zU_A{b8nQqYoE3x*_> zP|A=2AMa%M0|~hqfnLJ;ZNA+RmVHEMXfO>cPXHvmbcXHufqUj5DeCQTSrWj)D@zA6 zcDT%Hu8NddyR}nsKhM^Gm)|^Xr1wl$)g;?>19!OWYPNojvThoZnSDzG`=}o-$?(ln z&Kzee-%3n{R~O~;TnP$)5JZ(Kwqe{3AEW4ATD~!MIMa+S<)9(;!*msH-q*o_1tmb7 z{qPC^r9VHT^iOnH(f2WgAGr#6i$QZ{Gx@_5aW|?p5mYK^h%uaX_Ju|*VK=TObVv!) zKg7FzL&s+jPH_@b$Ws0Q5V5@^tT(x@vfo;v7+F`p zjVK`8i(c3Y>Y%WuEN*cz^pxLnZ6dn6BTj*h4=J~#%*)*r%$pKNmTIRXvw`A+0R!~7=CkE?)7Y5SnG)DOwO>T3M4d63BIe~s-_*W9a zDHxPM>}7}A_pyqEybF!9fn=7Bm==RRPVwe;7BxH zywqN>^&Y+8z_l^+S2I#ios(9*yO7N>`@2ZRFRVA*>c=%WRLtraIWM6aIpg zlJB(>lp64(xuw?Gn?JS#ZnH2(WmI*?bUVn09RT-I2rw{l<=>H(nb;9cA^#+NsSI23 zM3NC+9Ddh+`|FGp0p_Kb-6uSX0hDM5IurLMEzkxXh ziRH9bvx`{gx8KWS!$|g(rRkj#(|@Y7{BM#=cvpx_n4iZCRhh?P zX|9F48l0$_!B3s3$#)4>=604Hdo!!7U3=!sE_K7xWq&Ly8!t{ca7*%IMSxH$o+K&- z$dkn6zD*;nD#SfL%nSWsSF%&GtUpEJYTLjRv?h3f#m3Yd{^OFaq0!s6q%pr}C)Q&$ zB<+qVa-E4{vZt%}SNa9)YR^I%T}viFnv3Wt8Bd z#JeR0$Hc?a_R-XCs(cBTuegIKxbnPd^U2Wv<*ck~WHMW+&H9YUj zp{*a?ZRFW1jsV725yy&Sy-&x&i*)<)wv@=Nj8||IE7@usB%h6j#cqr%xC(}U zyMCOx_f9qbjVukt3C>RGP3WTETQH?YjZJH4tAaSlA6cs+xI{FIp@0?1iRwbgv3^pz zIxJ7VbkJ;e z+YARH6pu5o833Xv@hFo40TDS zA22`b%F27-EiAX`Zn;D&MnA3lHT#ma{G!@)l!iBgx(87@QNVV#T?v7FnU$5Ql}svy z7wf}C6?)R|Y_s5twurnv&v;|}qtwP@(IUA1r=n?!?%3$!-joWE<;>s%wHSk})fD#2 zLt#R*ZqP&P`bTffEYXGyyfJAoM0_NN2U2sN`$o#@jY^-#+MDzvu+@b{2x?tpKZ|y$ zD%d?o1ccfv?$C_6$3Ps42o1N`qGl}aMMkZwHRDIa$wR6d{R94}arL0zF${p$u?L{s zCvMW3VyP_JaipUbdlwHjMS5Agax`=5+E9N`VlNlS0;!nt2l61~QotDLHEzKEG6mQ* zR;t!Mv%gE)a9E{&vOf#l*q~_ZPOY~qG_^oumJdd@AW`d;h@ZWaj-eI1qtxEBoLq(d zsY?SD#08@f5wG03f`yrvi-`;fyIik|7_xm?nIq1UXyc^7&lWLW$w7~TQXBmXgc5GjhYvM^U2DcUE2si>hTLOL@MYP)3ZPmU_K_lkJBjMG0;$f z+VU~gT=L|50o0s}i&nE5_f2?V?2hpFykPfE{hJ>#Qh3@H+Jq)wz(?Y(+PjZlBFC-i z&9Qsfn*ShoE%%x;!sR1zxXt5aRYfp`PF;mxcl1dAIwKhrF)X~S_oAy@uTfmbO;oH1 zjrtvT@7C%?J3lxkMaPc*pEZTfT4jvnW&2}&?_h=C}#Md5Q6$p$Ed{czHEaiv}`>X+ntM{33-)WQo+@@Du&|$oq{+guP#75vuPXV(c z(}uW5H0F*X)8m>6&!Q-;7LxL~OA`i(Dmhb|n$c!b8)^1PJFsGs89O~Dlj25AsL%o} zlirp(yCqYpi>u@dVTYz zyx4!6xEyT#JF^HtO!n@cF@?FrIcmX^$YokLc@sT| zgtq_XRiJ&S{Fzcy^S6&nRaig$M76l+cf~_Hnma>dIUjP_4Rd^p2TX?QIx*&aCqL5CavSK0-z^ zoaYE0m9Uu*iz+e5D8XHMS9vxrM(jrwZAlRop18Q+<8iNR{gF}}x)EAbrz@+=8M#etAzKoIte25N zhluk*JIfwxBbNyW#to0BUCyXq{%lLq#9%g`q{w2bQD<{2*T!bVk>;1G(xhd#vu=x? zF|}I2C}r?-10c_t()xyOCMdaTsb;aTqETAybKmL<&O%X{T=b!>N(t z4{6`LzR=pGm2;HVTWs z|M+3~a$@c@qQ^(;O_Y%?0a45UGWBw+shN?j@?DWvRGuDXrz$~7OY+|>rx3e-2S60_ z7C(4}TYox>frH7N9ljHyJ>Hh+7=8gIQ literal 0 HcmV?d00001 diff --git a/nirvana-ios/sounds/leaveSound.mp3 b/nirvana-ios/sounds/leaveSound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef172707e1aadbcdb31f3140d9054c1e726c4c28 GIT binary patch literal 29407 zcmeFYWmFu`xA!|}aF@XccNpA*y9Re=aCavpgS)#1g1bY25S-w_og}ygNCE^4%q4%l z=bq=>^_;uT^XmR;t)A80wP#n=w?4hAszq6j4+-#;*c5b)2;>D<`e{wD;%F;HL!8(pJ_7 z0I<#h07G>xfViZ;JJia_#v5d5W9Q&1L3iHWO9yhWmY_2bPzS5K%i7pGCcd_w?g8W^aUA;v8CFr2mZk9HpPy2sPbJKzTX5#H6K_~qu z1N2lFNY>5M1|-M@=ClHXc|klpyj(nj`~qMw$5W+w!Q5bBZm<9+SXfkmPZZ1p`bVOB zjsX()w6+!1mQ(mgX-`KIboSof?xNh>etv#jetcYRo_5?kA|fK(U|w!sUd|^APOku0 zZ>T?~s~7zr7k~F5XX9n%>EQ0|;N}YY;}dG>=Ho3vNB5Ku`e##I-2dg=)r;$I&Em3h zbK&-fx^wezfw})5#_H<-@8&Kp|KT5A-txXrCH|xK|H}frbOYRNxV3G(+g5e}wX#u`lc0O52$zF{wWzf)l$X!a(uR{C$|KAP zw&v&Kg!1!(Ir)S{_-qA)t@(w8to~WxlM6XFD<7Aq*8J0j^^*%5ULk%FsHHWhl>pz9 z3rl`}P7xknK29q>sEDl~7-|FkcN?EvXnHz4VG4Er|Lp1y3Lx>P20S@aa`5tY^9=ag zslW4~W8?AnqrV?IJNyj{5a@5cCJMFs1C9ip7u46r`fu%e^7Z#DhyR5-wt@n@B7#=7 zoFc+jR-D58`~sX%Ymuiu5fHZI7Zk9v5wQAu_x;O@pS{hK`#;_KANuY;^v1u8t?Z$$ zb~aB}DmNYX|6Ha2x4ZOT_4qF;{U3n)m+`+V{U=QP;q{-D7w7(SLH@G?#JT@d7yaGf z|8@mGHTv)8o+86jOyd4mQ2K`&Plf)UO#eXR|6~y$&|ii8xAgr>uD|5^Zz=HKGX9IM zzvTLFDe&Ji{)?{vF}X1Qy{xfueVQTpJuO_)CHRzbOE} zYgD6!0sx5L0q<|&|U?R~7Fwfx{mo6J!+`_V82RdVV;UvLmBdxB_DQSiy#6?zPb{yh(jj zxb6>wOU{2AONYPmfJd+sxWT`}e;*;ssG`pl65ffV;}*;xVfM!rhK>#vQw0>cYuNK4 zdC{W^Q-m~*#I7Z4W5ld~ivD_efCld0kh#+{X&hB))P_0a1!E zGP;KVH@W4jh@F&_rmMXSN$xC`i9FzF;8F~oEQ_R$m(M76z?Z%Ab1|{Tmx;EAex4*8 zet2XC3u(%V)xJ@Y(e`e=Yy7-a6pFbH?HU3a&g55L$d74TbO(dP&MWuyORad91!L?7 z_~P|5Q(8W6o|>E#H>YPBUrpF3G1Ip+gl3mmWawf8@o;{5Y{y13PUiRZ<8VYE*6OnC zd@sg%XG;rMIZGnp)WAEWYQ{H;^>WTE5#rb zJ2I-A-XS2tDM*X#&8F6$VtZx{79Smf5U9e*yMAi}c6ugm=6_P{2fbHt^?k8fKJh8h z^j^p|!){^gF|9`4QJhmywQHTrtZh$kV%=Aa*}FC9OrbMC6QfK9deJy8d~mL9fC3|KN1t+ucw3g)|{ zA|h2s+eWfjx(HDQv{7V4m@JDlmrslx)pVog&`Zq*FQCsZu2O;{iDa4dI@n#~l4J$= z=OgHKg78i6TEBM<#YdGkgrcgI<}*C_=}xpgQ%Imjo5w>>O_3RBj%_JZ>_kH!&7d?? z_D8EBo&`GY zXMM|Ktf`?kQDJHHFMx7bCL|P$Gz0ilJIhP`+99UZ4B5oUO<0oDQQJz|fRBJZJxUT4 z`)^Hb;V=N6Kj#3C7P(zC-k`e(u#(1fpsO}l`^+QoOnG5?-Zdx45URC);U~EGvrt3P zgHO#DahaAWKb3V$*$CAaNf~LfI2?sT7CDK@NX6R9$(YAaUNz>Nn(9r{nEfaE4na8? zmhDzUZWJ6x;apNzu^;IUzgi4mQ|!!nRiq5&NWElFF5>}7(6btuW)doz%$e4NC(%VY zgzmecIWRFs1u9h=6qEvnBuBxQS?SG;=z?h_mG9!3-I*c@%Cvk)`c}AO&^-zel(KmBoy|mu=f|CsH%r*(jXpxz*_Ka_n5)h%`&v z4q3VJWmue)>N;il;K~jYR+BjZCfKzUc**j5ivu1 zJ0+c>Kgc=ka{!Px13$p%?UK{olq9fh3kf(+5MT*n2a&bUn~zF1TvujQCST!j zsYFDR<8_%ypifgyp+){F%Qof=SmC78I8$|^HuQ|kmniuKE=XIa*zQj@!uu@G3zKxEu+IamB#&}e|zvrCwqS`d^xt%nqb$I=qakH zRK7N5DYp+U<#g$xlt|Uz|HUE(Xi*3%jhp-xeNya~7?C<&wt~2)En3ICtLs zy;!Cqdfd>PNEkGc9Y)bBoEy8W$XSyssV<{k$u%L>L+JQ9MOLE|NtVx2I;6^sqDz@! z2xnD(lI2%BTO2c^D1w(rTF!UmV0C&YD8t=1@n8@(T z(Fs+ke?plekygIZqD9n|AQB}ivi9%eT%vY@1JLpk4295^>{TEP!tZ^^Cx0sEHyE)i zMcHCAm5-9u_^-yt+2u|Yw7(5__o;2_O^-Xg<74LMpr4P^kGFOCn_nJVCl9<#@a%QY zmzU~qD_kD@j;jj-^nbaw0MB-3juTzZ>R18;ICN5Jeye0VgUUoKC~X(=6%SDU*16&M0S;e>`@DwplLE%ELeNAGW_jtJ5$6PE z+hRuf*}--mh1Uhi<)czxY6(}UWjQvqpx$ z;%j?xQ&S>Qi%aTGr4J#{jgAQ%d)P#3@=qGA9}ki5eZ*ozh(|%p=}&LP8}!T&Rp*%o zSIcWsDf~fh^DQsnS%jJXqni{!SqgaChX$Z%is5^hf*~LQ=sH3{00sV(e5MVh%Hij_ zZ~FH5-$b(t>k7kok5I7ba8|Z?3^4Tg?%@wv9+HIWoeVlC+BXpn-sSagOvJv3Tsj+h zvj;sr6(=EwOJxMS4`|qQgFhvk2CozxnI$@b;k4p1fsKxabvjPx3T&O%@Za*Z&0l`w zT6mzPmp_Y;O%W-0^}YN3Yu$MP_laY;rVHr{_^-{%FBTWX{3*x+PE;mi=`xE>xnhZs z#k}_2(m<-0cJyb@3vfm@7xEgub&t0%4!c@)qIZ!gItH3}Yf=uT20v-`@Ru1er91VGrQFc9_D~1Adt4GiU#9FHqrpcdll!+Vi%uksZUqExh({q73FYt zt*Pr@sb;dAApz1f@;x z?YXiUZ&DmAPThVy#!3~?o8TWTy558J7fTDo9?yNA=n!$ND}+CG{L%Ao=>fjf3%AJV zyx)EBy64PYcgX;eXR%g!9uT$jxmg}-96Pz(Uv8E16N-Pdz+Bh4ObO=?-p$iAL zt$&bXO5y-K3$;|-Ny-9C!O-fE>0HV6N|;Z?GUXyFB&)`-BG;g=LvbY@~X-x95N zqj|D{(@U#uWkJUCCL=3#YTpirezqC9Q8Mo+;LTgI6?K?tjY7GIFb{V&+S55mv@n}? zDtX-BfcER3j}LoI=Ps4-_tuAfZZLa41QYgo@qrI6B4c+uz|kIO1p=gtU^w>2_bA-k zK0JgidP#&=4eX_cgq zl>msJYj3mNWs&+PM?=kN**`m9=VuSB?9p;)Z zdKX=#b?1pfUtwTnR}~NUabd8|J(#uWc_<;1f*{W!HxJ141T;qxq+BlNqwnKOFNx4rO1L_!JA*N;JRrf? z0Xc0LFMm|?8Ml*UX!Faj?bUU5c!H1l9_q<=b^N|p*yt)kQZd@I7mQntgKzMs^>v7H zODx%ptOf@$SAOVwI4-$e{rF9_#3&i|+=F>T-O*%@{hMkQ+%JfrEB4$wia`p18O~no zx=^-vNXI*W%(5{2;T6b$_?N*eGWdnt6y`4pDwJhC%$YLzm^8zM0aXP~Y>`1j2GlWl z)28+MB>;fLh!Eb-n8F#?B}d}Qf+LOBh=vGY3s*%7qllQcfeEu@4}<2>fv8w1XmxbA z7=SV>0|)lbf->i->7c^AC9zzld>7IyqM8=SaU@|38dAyJ z#UCHFFyoVUwwxpHnoq|mx3h`uj~a>F%NP^UWRBOfLUDZPapld$TxP*t#aDX4iyW87 z`Nk~Pf1$qUGk@@Y?*TkPS$$pX_RI2A%kvK{=_M`XQLQ(=&WD+QJ!nCs`q=}FqrQP^_aahjR;+95c{3*F&Jbzy1 zmAjOxlm%~b`12XM&k-N}M@!wI?jF38H)_$=3!y(0Dx;bs4l@*4nA;AWEvVZ}jU|A|^CLNY6=^+2C6a>gzQuL#** zjV^2^6p4C@RX-94M7(1}1JTN4?XeKqP=-u%u9F(l-Rp23-XGWN55Bz+{=q_=KH;jk zTuM0z5fwE+=L&%;p0J-dsB6neA$&mZn6|~-T8aJH`mtM*}mus-3x_}S-8B0vc-|h z@U^Dh2QnNTmv#8lycC4UT;v6RpvP8g75<(*T}egw+2gJbe*Jm4rU4@SW5_c+Hx$%&#YF(d&8hSLFk4AZ|!cQTvp)a!}v_W-q8PyTHAbGQ*)V=DU z;J5=kl6i@Ob75rT*=FdQ$LgXIXj)ux%=j9sQ=K()OMhz3WPxY_H@+CXi5y)}&-sK* z?M!#`Behi`yx`I9?)3Z&ZUFB-2%7e~?|qu-+QDzP5_`=i3?iG@x<0fNk%`oiuXXcl z0YU|_zdZC3;P}MLc!80>Y)6kijZG7jcAY%+UmnK14kRnit{z^S(PYk^bypR{%6v^! zD^ieRKm?!@B_^o~q9lx(wfoWm>=^`^yw__lAnwB)M1mv|a*4E_dqt?x)lT^6h{Z@? z1IK{$DyZ9v$hWOtqd`h@U&E8pRqKO1x=imu3GA&lvYrgz?OHv`06bm(P%Swtg|P(9 zar-(Qg`an?DzX>K+wK=0YMUKyVdDDz(0qn}peg)+noZL@)_PoEi_v28n zA90_i#eUqd>ww?axxpWed_TYb4Tq?@_;@BT zk%$Npz&aTnjh*1uKFcrwzkWXd(duWzoE6ftt+L0fIJ#mV9yuvaSTnhg#)02AUH?Fp zL_97LXob|Nr=WFRtcHI{!0WS?dHB)8n{o)+pUItW7Aiu2KdKxy0uT!bZIVLDw$m<- z2rKGi|JJ{fYL_hs{3+bpX#5i+ES`ylM-6Y#<3fx~EnkPn@Nj4w2{Rn<2f5lY`p{>2 zmg>u1Fo577S@2#ufTAsiv;OEpYBJuIpEN_Mk2xSR0>IeBQRN<&Ze+cb)ts)!b>wfL zn|K38>@-j{GvnCbXsc~DIM8e%u+;iC_<$>`v$#0}LB^uz0TUr3NMQ_{)wEgGFFR64 zzB#yk^v#{mz*)&nP$-+L`qosFD4D@uO z=hnC_6He*4^XWh&Nz$Un3l3+S74}&x-~D#F)&!hftb-qKR_PtLdw#@R@P+|M`n*ny z^G$iX{0v_ajWC&<<^HNTQf|x+E+(z};`uuaZkXuu{QSPnlLf9v--(l3L3yD1BquU6 zF6IaTV2g(;BVz=UqQ$F+;Az=&u-L=gA$`e^QMtjH7bqz!1AQ-}(N*7pJC{r>XUIsn zskGfN3Acw(0mO<)C_d%ZlTa_#)L{emkc+9ufLw0@z)S^bkRi}nyBzv#&NyC?=IG@I zGhZjs)Z>|}(Rn<|LYy$xOw;ta#j;;hO=1T#E!W+~(?J;-CpRul9@yGdT5W@7om=ww z9raT3L8x54-z|IMNVvbW)w#SpzN&Bfp|;dZ69_+9cByCEKfI98t*Tkc_?)35RKWBL zexG<8P-pK`gHf4*#szPhY8IJUEc^NrVZo{qooemX>(l!nnBpMkbg$lHl4ge{+_&&G z{OaU#){U$RKmY(}nVa(K>oDQTk+{c5OGA{CAf{j^_aM(F3zbj#UYCoiiF09HIY?Uk0r_M4qC zFL&Si%A(kgkyGGq4Xw|<43tp2FCii4ec2YHNS417NU{1EHSVSF6^zz z)?DgPycTn!xZM1Pi!xOj6=gLYzo{e-gE*>mF168eJd0WWUD%gP|p3 z!c8h7a@hp)V5#EOdTQ`sm($-6P%vuIxk21D`z=j_)#-01=1sfqN5A!)I*b};00;)4 zeYndDgVi75RguRaA{M0)C{Io@ayRpL#Qm68C%~g0)JOFedp?s3?Ts!cK#VF?elAB- zkve|r%C<$mjBhD6tk$p~cSuE1W$$`LjC9Z8`6&Oh&)uY+iZKYcGj9f|l^+GmknbN0W9| zdfK(E(@2#I+*!Y7oSkCeRqE7p({5EVcwLlGNGy!P|yA{~}>SVbP@3BlQQj*YK9}!#l}4A+25+2ME#&5I~yGVhwRpO7#*< z$wm8vhe~>JK3{19?~N^Fn;zNTqCHH1-8xbgEg_JYfgB~ivG|wnyV~{|ki3_I(Ch#* z#VMX>Ug0m6pE`YVPxnWRc#;qU1xNj!xt<5xE%K~Zvm@2seA*qS(k9=V zn$Fa{rreBhiE;w=fiNMNCf})qm((rxfds5)m(ZSdsV&I91Yi3Aa_n0!!!)(t>sE56 zMK0BJE{{EbH?n`n1fO3$Ia}VG6@%aDCifx3U?_C9U%+J6E6FI);!M=M(n?=&!Z@8G z0y1mqC@Ig5)D26tgE0$VEz#w0!uCY#jIv!iQ_SaPx?-}LUNWWNQe()?&fM-ny1HE< z{jD4p10A8vGWD%J@C}6n9*5M*NbVCKr;SZz0q>2SF^xwW>4|iGm;RrD$?Ct>&o(`< zo-Y`$$x|f7G_Si9lqHV2+AUo=%k;45@&GDKf=s6D(nX$WXFf}!S&HLKuSLfC(#r&u zAP%tjVs3>7tR}7f0kLI4tHgr1CG;jze=lyoG*I=h`pw)2J(B$u?0Ch~x@= zHeR4z_fXnIw`K>@mu}2+w0v+?KtxZ;lPYcT=!_~y&5u)$5B)Xi|87N3n3lgVqubyn z6z?>C(n6}n>_mO@)eB`$3-ETwXHo%QJ$i9kKkqf778dzW@3-3k2>>Q)xWzz^1jk!_ zmE?rXp8F754`o&;pbSN8D?W8Py6XInQp*tyaYmQY;1Dul%BIv5~Vm?uUsK0boeKcCZd0Vb`)01G=Sj(NHVWs&8xjv~9 z01CE_>WQ*ovSNuyFf>+_K$(%Jjz<826T9pxT;i*UrT@lmoJf>JOjA?cRa2oxuG_|Q z4ZjcUC|3cDI+x@0ZhF)#>&sD>YT!b!bMRoHjG5x`lE*1lQ$3TG$0>Qiamqz*Nb1~T`ZUm0>#A;Q>Rx9%*AMNo z5ORLf?>R+7gO^g?Ez-s%2QTQ(208gBi*iZDo*rYV%j)Vz=yH%9UN<4$EU{tox%y=e zE41atk~o;YvrzEK%Cm(^4)^LbRwuiD#REwJ+TT2`H?r8#%srbqwnO>eymf+D+{?sg zggC6yQhwcD+HiOir~<`lTs}IfN&0pIKfixm^T;g-=n^}WfZw%E^~W~?&@CR!&`q~E zWr0XMpjo^ZF=trMfZw%>0N)`jT9nA7*aj#wy7Ev5)0HeIdu${@tu$0j75Sg7i^bI2 zPRhIfECotx2n>-_w^Rb78xFb=Qt9c<*s&AEoZ!XD$!_g@iPevRjeLeJs23cjY_!cn z!?0m|7nz`{WfjK252C?kdFoT@KO0+l<(=M>R9RN9+uvd2>We8pV?Q<<6#Sf}VNl13 z8T9#J_(uJ8@VP^Ku1QA%Esld)%g_XmhxWAnALL+0ivW~o$|@&r8p-mqV@cw*{=hl| z=DPRm2;A{|-p)k+;$e^tQ~-eVgP#mJGs?#ZpkJy8D8Rj`0hw>yF)jJBTw=WX9tm&o z%!<7w^NRr`LZ&G5Iq*#)d?UC1vv914!p3f7vPWuAewm-ygUY}8ss0~G!l0*j5F!M8Wd7%)n9Zf4MpR^00g{fu=^+thT_MVz!+APi;oK`aT7Fb(&hFQ5DkocE>DA&Eo*LC3A`3CyD}h@hk73A$fX{- zre^79k@KN`%rt%bfVIrlpxgqJLVzLS1wD*kYF%pZP*V{G1rsTTs0cHW>$b~z;L`Gy zJpz4*sn{Go_W&RlIX*IrlHGT~qOY$X%TXg`fWv-{-4;H&7k=(xSO7#ncz)=Cyq?rh z0&;lhD0jt+IGhZZY$ELJ#At+PFGyE^L7Jj@nRvAppDB&JpQrSyfOUJb9AI*_Lr@mh zTw_Kh0_blN0Y6dKQcK=7UV@USFYE!$+)IX+JdQq^0GUejC{JICvP8o+#<<-H*jPtm z%~fElOCU2%;4jY#-S;$Rx{O#*oH%t7Ae7yuvq!5coj^O;)v?aC`f`rrQrR`DVT0(lvsvyh7es^0YxUSnSg{MA@WQ zo-rel$tWX`#Tdhuj;KLb3T7-5I~M&qZ~`unsuuvci{kfIxBq<@!M zTA%V`iBiPfH&yS~b)=7TS@%T*({T7$G7?3T+e#OEh5rng0E7Q^42>EQPzm)LkdXun zh(%!e^T?&IKUE4zAenYdCtH^!*w7uCqaF_wBS4PmvuCt&aHK;;fvIdf-Pg{0Y2I{$We)M1X!Sv&M4MFKWWe-ny1O7uZis|CfI|%aw}Dmt8w|x*Z|sCaBUx|IffM>o zi5z=}fD33Iqo25U74lrm$J~N=>WvCfv#dXWMECT=n6uWA88-35oQ+J4J5?X5RRI-K z!;YqKx<+lIw2aA*3p9B|A(LA@iSYb;_1lNnB~}v38j34?vz`Y$KF9xi$o*#=y8uuK zfTH)VS{4+C++oE8R9dsGR=}uzVcP-#S(fl(xLZcwEq`r4XNeE%(H7!YmU{E{4uO@e zM|2*cbAdS?@AI%GGCuV>e^+!h)G`58Q9qX>_vM+mg2Gkc;}7xfSwv<}dMKiFH~v;6 zbVL+U3nls8!X!N#x<^0Pp)L<~o+>NQ=R`cT1=Tk)=AiVGX@I6L-D=JokMsFC_W6ZA zfPzKaMB5IIDB!HD5e`9)l`=t2qdR`7%=-2C=R5Q7#vXEbqJj5T`yU>kvwn48lLWu| z*=v4Oi6$OrHODIw%-6xcNOONByY$dw!Y`<)!VYHrwx5k_&P{C%vG+_ zakQN=2ECb20MyZ=Aj_<~(>DM^DC-!|-|)USbgeZ=Tn3jR$mKQ9j_qbv+uEn>wkSyp z&=xQoS1#LB(ka-RrL@_JtQFd1B>(#Mk^J$DSU)0Kx_kJ`2l9H`iH&8AUlwj;Kb>*< z{YZ5?uRAYEiDA+6N6@5hBX*b!0swLPi$jDl7Qvp%S9t-eKghAi@4%3^G{#T3Dd!AI zJ6)hQnWPUoRCPU~(xHSl!BH9hsaxuxi3^r@M6SfPDN~u>ldK7QG<$O`yvr_PzZ~C& zTS$G6F&OcQFA(@}gQl7fzyo^hM3#O>bJ&h5jH-J^)8M!1RyOU^dh+@aepUT%Nh5j$E_mr$^?`*`)XyXj85QSf`jeNa+p zMFN?lM^O&X%S`Yb)Y{q;`_+Fp0Ba+S?)^!}-}~(*S+}qV?4(`!wKvZXx3X=}Xgtel zb)&TNK;KH_4_PZqW@tgjY()*}WBviNPL$^g@qS}N?sS$ERF{}?)SJ|4V#M9+`RHwB zR~VM5*Tr_|AWfcOGO;T2sM1;V1_OXn_gIapi(ilV79pyr*U;nRXfKo{dTEx z*2jqu0D{-+k`@l8!y!#noGL%u0|b|7#C&Vf&mn^_{Y(BrS3LU1MmKT}^icwgTDK_% z8q0)=36~*yALy;w;U3YVqj9Emkx?POgR`yK%@C!k_nc91;KJo0i4pG$_ivF(cue{8 z>VR?e>XZ=S`kmvibT4CE1c1`Oyj7VXk?Hj~{GKqEvfT8xUJVCix)}b`s&B77F%S#(V$u^mZ z`T^bR=N7sX{+6y>+lnEzKYP_?;W}e7F;UZ*)KpC3dxSVX-LGHY$*FQPEE|LMN@yMf zLo)tN+&==D6aZB|aXes#oRDCI6}G_L25q9ozx z+?t#%#NvwjPc8LweAqzuB-1y`L^H^7<1`T3o?$5=b4cClI=#O=>2P#z z@E{`5-0~|gP$-SVEGhapYSd`Ku$ig={`jU&;{Mg|57wOo2bSMhQ39&TcMs(J5r^n| z#t}wN;Vu(D@7+ppgNIe6#LaGFCFU*r3XW=5968enP=q`?@Ni^M3C8rOkGE9#dNth4 zE|IT`?YUH1UXQ(c^Xu+G1b525$JA(9v_BaH;5@H&LeETAIekwKnuxZ|zfl`0RlE{&NlqK?==?vWE%>_3*c^4$iObrmD$44`y0D7*8QMRQ1ce zli64X8pqZ}UY!rIwqKfMcn%vZbwyiZ3A%@D&pWeiW`-VnWBzF3Z2*QA;=Sq~3!z-M zopq(X&{GH&@L zm`^r{0VJ&I^mP(^(i^F7w<>ZCy@1F4a;7=udhB1RVwYRJjB8{~DeL9x>??xmowISY zf#1x{UVlvtrTr3nYxe8@!~EIhM=|H!#^6s^tP-;c?BI4rX_N3HO!3gT9)Q5pJ!~xQ z32eExuP6v~N=5vil<3K^;@NP4n6v>wljH5!Xhs0Sf^t+i2DN4ulAlXVeq&4zO&|REvNCR6BRPM zb;cFE@5?wdxa)~+XEk?^e#OUL_2vN77k*MI3tXxwl2tO)zQ4GhXnhm>@dWM6Q;+jQ z=W>Tv;Jd;L=J;V~!IU<(S{yIEC#tmzdaX$Hq(CVh#wELniT0GDRDdBkqMpr84K@sW zCKrtdh+o^jYyFg}TPhdNHeipU$6@hZ5ce%Xj)2C_SoRj1CX>T=ZFkdG434osx%Ths zoALxwd2IbBcY`{GZtDlgBS97!FQOZ~>anIYKp8jdui?Zh~)qT5P+2{%vr`$p4Ud?q?@_+Ye)UTIu~kF8LlFiCX0^CmTug z)?&Dm3c8aCOz6U)mT}07ZFsV2qohxLW?&Mo;WbYdTHV-#bZ{ zLr!?x_H4*L1SF5KO@Qw^Z%}?8X9V_E8aGPQ&DK8mQ+nfq>XS=Zj(7R%!|~&4*^w`J z)1i44XS#CO+x|}UmiRw~z5hIo`{x757D;57zs6%!YCF2j@uK3(^L2YPA_y#n5s#if|3jTU zg(8CKu${tGHUl&^LjUHo?ooQe`g6fc$0#1Dq(!X0SLzuWXJtY6>z# zHhSJ=rYMNzL)qNLm>KI zlaLhi+|WbI-V=wC5M>DMol>j8-Rt!SwJL_>tAmNr)%B$9%~bB2#~D6T52dp~c8nga zD;iU&?Pui1qw6~0pf@rbMG$8__CzPz5xsmoY!AqAtsw_EXV)u!&~Nf|HZQtAeR60I z15JRKxWq#KPESXd8b~EQ%*Vex$VbGclFFu3T_XUm9^~WWmTC|cHRjAi#mW^9w|B*00isL?}+Pabmj&8vPDI!63o>tyHA_Ra(tG?l}U@!Eh zv^+R{54D!)FbeRG_Ic`5btce0aiAeu!Vm)aBQW8C zG0@2{J)Ys>ZBY+=)Mbhig1~1|R&yk0XX2BEl$K!w&&XZzS{OR1)Xy^$#tc89xX`m3 zZ%Q67!%+7V%c9CjySyw1t>USwXeI5ubOrbUrs&enpy=Fk1NX#%@gy9XL|AlFj*q3E z#L1=*e5z)XJc&6lP0zumn}V*mx90IfjM+_FRmO`q#+7HU(@u}Uf{)!0Fe4V?h9^Ip znug`F3@jW;gNCqjvGm~S+Y}k|ecj~Xi4`6=mO#xDoupm$@5 z?FBd6vYg;VXF$x9VVG?H8#~{wAKG#`c<{%^-uK-PuIn@Wum$}US$LFagG+xaI?2NT zhVMwsee*F{6Q0fxvh6W`(TjQU-m~gV4$-fXJFY{H=#9l=GL5^4Yf^Uy3JOjRPspha z8{fc?&NxR;I4D;N>qhHrwsUoXy~K6U0~J)FQh~_lx@ehh2_K>;zG+}KLG&mSc+Ezr zELv?lqoOFrI4%23$%=l!j`;d?i7AD>F4UxTay5UC0z1-939JQd~l9>=rcvm@9(*` zqgXM3X>XE}sACRQS28zrt>^r%Dq5l5wJybyeh-0Px}zpVbP(rSue9h1t;|$b>!)xk z<}&Z+o#@O@z1U3@A6yUQ_3hxR)uY$)iqY53NAy=L40&GJgpv;kGWi^Ug=RtT!@;?O zRyUudf+r`EDUS*J-nL3w&)? z{p`H0cGskO<;=Wfb>rRCT>1;JVi%YKi%h*93k550(HYMQBQa}r{5bOXGCXP%^`$6M z6+TUfghp17$07QY1goirek2RcEY3DWc~B>amK+NrV?%cR95X!`sqy}+*Vd|7&3xKY z7P;2ig<#Sg6*|%y!Zm}TxtO-HQEfM&fsLapgbQCi-wX>ZPZj-bNYb)*R>zaQ*UtsN z`_jQ?%KEtFd8;2EU9JvgG#GmXRRw26y`TyWpBdOk%alQ%&34s<6Tn_v6OSPLFxy(A z^inuf02M_pkpqFiF2(9cI*(7Yt_?A3FjwE4cT-1Q+6Qf#)2lzoy?6ZuKqfRFyT>A0 zD;dwOvzEQoZhAqg)~JVYLu~!tS=`isHonzVc<}<2_YL7n?AQ+phF3pArl;zCbe05Z z&c35!jt@=?xa{X9o0=xGKQpvX@i4k&sVk_He6P>WWkcsFD^;Nq*LJz6b$ou|s!oU! zSR^9QB(S?y`Tg*z%acpwQHcNKW25oLB zta+!$BS_PcUye3=Q!>p%mpp{;0>uD(=gL6%dV(F56$lEgV7kE}p}W>s4>F$g8b z%(C!M6t-uISK9kqY=E93WY~dkEAW7Q5F1P!IYKS2A$jCk)f?=^wZ3~fGH~xd1m=zp`bjdP$ zbfdhISzEaY4V@=LIeCVI{X8^`ovtk_u{gw8TE$1u5S$w!po})S?53tj$cIR<2iOwA zpTXPn7H-L7Y`fw?gIMAp25C--v#~yweD<(f!CI+`uBbZB%{rtQ3hVoDLwCjw}__fad#T zuCa|=)x&F55>!6;p~wnTa^Y98;zBoxx85B4C4QS5b02aj@@tB3_Z+{~F|Ew(IS_M6 zb>op(Z!xrUjv!xHeyXZ_c2)w=B?ZB5Wd^je3CIz&*G;urdVhJR{(df8TT-t?l0+^Z-fJ zNkpDMo3r-}=Ky<$2<@MxiMIBX-V>_u47^n-2PzK`*i&3qeQBY;C)jfyOF)i-LOM8` zO6spg*3Oa{`-Cx-K1@7-6_Zte06el8gT&Q6@JoooAKp#j)`MA>g1i2eEnWa+dLn13Ta$$!e(Cky<}b0&+KsaM2qMOE zrs+x*8*SxLkp|yqi=+ftUhVQHljX?iBjk7f6j~EwW2ClKWVKhuiW+MZ{ezq?`T+o$ zgLzm7{bf;L1aQ-aJDt!6pSbp`mr7<2?NLRd{cTsIMCcUjgOA5@)-yKV*c*U1D{kQ9 zh`gyQ zX*Zw0m|7$by&E`F)duNpx3L!uL_KL-b?^RS(-7_ur2w2z3;!V63)n zrFH!DDi$6rDwLHZt>s)%6j#|9(S!4q*e2|VOEblc@wtbEF~nr09xu7VDoyeYN4Xhk z*`mt1$TGhoav6ev0%t)PotLZXagM#s;kv(QxK&kKDDP2tGV*R-}^eA#3ql5Hdo%iT7K8spyxvC@2g%y(S4-=C81bp{*QKs+6iYQ-r zp}P5>?|j-1Fl_Ym%c(nyVPB_P0_tjMnSV1H!G~C*$;*CbgQ}C3`|A!a>W0VCKAe-) zIWdBT9ltsUe8=oHYJGs?z)ur6cW$i6U@9$w*(fA0lWN*0jt^t7Yb6rD0XBLH%O-bu z57ecuj)j|7Dcg-7%+hoQEL()cI9-uOqo5O)KVBAc$s#rcEZVua31FWoQ)CGCrd6-a zKbo=bJC?Vwd*5|QTz1H?J2jDema#f*)=cV~9d-nUsWiX1lvcTLKU8BQZrCNw67IyCpeV7_qohijIwoEn(!ae*AYzDFz%VM^!8Ua0jM^M@rIYY}5p76;CrdyNNUS$j)97rQmL4cAj2;Zmqr;iyW~}XR(=b z`S)DzAL~#OEwp$lMR~_eG=WKJ^*NM{ti}LSSRaN_0W#$*d(DZm%61PC9I&D{KM@P%2^wKh-=lkh!V=4ms5r(D5U~a%W~|1!7tn+XTG;_zp|Xp zOWjJ0viDT9{GC}?pfQqHIq*4vfxGqh;3=Yx_N=*6(@H5H``fafG(JRhC-A+=Zo6WB ziTGP19Tg$WJo=5U@K$4omY*s9UAFNa62S23+mGmYwz&VR zy|W66>+7~X76Jsfpdq+hLlX!d+-Y3@xHTFG8iED4U=4KR4Z$1IxRYRkZX{T64^AKu zBuMC+oT~GE_tdGn_vt>}vtRal+7GLKHRhgkkFoX{;G13)y>^_Sn;K`%Ran=PCvUsg zE*f6j9NHi-hew6eG`mx4XU+ldJIX6l%7^cVV{1A`lm)+k;wZU@J*5qL*4D%yOJLum z4MKaiE>Pg!j!mn6=YZ1M+x$gt7v~D=K}?8ZrlMYP-2{)OtMf0i^dbsv4nvJM8KoC- z1)*SVWNk^S1kkETlf)RHeR0&+WR3^O;LI2d{BKQp9T?L~gVCg1w! z{bOt16D+oy*$JJR;;V9stlu{jSYfI% zzESqYwtpN)Li11#@L=I=gkcjlo`}|#nm}Y13MX2xP*WR*m zKsq}DTwa03mEwY-mTgzE87i)*mzGxF%ra?AhTBhKt-D`XjCyqrxH*Y!uga%ZPWC|5 z0GVK|B5L7E;@mPqG%VdSLPdMqGXrm&ex@dqm&jD-oIM;Y+Yom?_%QBiP*``HbAH4D zq45@}LtV5eXS|4O6IU(aLxs#G$VkQhCURcVkyvKD!1Cn0IVPnfS=`7?gM}AM(u5!} zx8AHh?j&w!7qbi7y(dT&y6S*MD??S=7{TI=hsZ`6zd?VrXdocT1W8=Zo#%9!a7sON zqib`_b8UUls`>2IfIcrtYri_3#r69-e9)aX6V?-B%hntXF%cLm6Nsk~JO*L=Kb<@iPb-?uMJn zC5dJ3ye&uxyzdIaRy6MLbicF}a{uWPP8UR?^eM$=wzZ&8TzyLIeLW+~Aqk4>(HzCS z&cxhU_|IPXE3ngbn2ON-%e2$y5t}9mVn;I&9UeT=M6;Y#^JwZ=g1&U?m$x;FzhP2O z3(q-BVY4`s(Nm{8TOxRE#oZK-hagH`{)^b$hrtiGF;pV{6m6V7c{C?v?Rk$5T8Loa za!5w0SX?qtJ^mmaW%-Y6+8qBh<|pI+b>7K!W;0ZNSLW`vr*XqSbNTvDp~0ip~ZhC>So_^Z|(hE>0qc}gs|n%ldW{S=-# zdqO7ryCMb6o_rB*c)U{7t_%No{@HwH6l+6zYL|iOoq0MFx1C5J_0C6a9*PKL;-JQd zv<~U1H$I%tEKb(#mHSw9|01_uv5AFy9Nd?QBeCgG7oRA@z6=tDP}Rz~qXN2EFQOSv z(b|>07||Vzfxemnw5M=5&~JJA5))uq6^~AHiKbE6o6}>eNK}-&2@ivBlm73 zn3jHvf{>Fw(4E8M@HiLJ+dqQX-{8s6sZPr^^D3t}3WLslS8<3c zud;i+$Sn^E-d;b!jNUsfR6`ujCWNFqpyd@)P$?mNSx1zZ;n(?&1XA#UsKK)HXwe+KFRCyD2nr9(v$zq zNRK0KAle!zcGpAMSAk=DC9Nq{l8)X;Bq9<^cGn*J41Y&Gbj?J@H9$@a)3)2hb0#7e zqv_CR+sWtQ;>qT7I~USi)=(cDKuCSc(vR$CYr$PCCLm=5fZlGDT#yN^l)Y-`tBj2^ zAW|SV7BDN$a2xTeYf6>0^Un0Kl>mPI(La>~gW)?(&!i_^00973E3V2Z9{c*k>P56m zF(cL7{?1X~j#FUm6zFeQU;6{|AC-IVs&Yt7-4Ur-Ss>#)Z_2y>$DJun1ee+HPtUV` zeH9~9Lz<|F0Fn-_-o(iBwi0{1>)aVu97lSa+p$`WS*2J{kM~LwEX2F`Q4`%QX?Gat zfxN^Es*$IU8j8;-Iuyt6+LLoTqJ;1Z=M-_jQ07O)-$}7OjL>gIV)Udp{~D$pDa-6q_}ph}w2FbZXlD1W)ol+jRP@ zc=YB5)3~c$BSONxN-*06%mIcduR7lMqmnbiABh9>4_MQivTQ+_29Ui%>Ky?K{BFl;YU-WiVdyZ&Ba*(SQ0$N~sXN zmYa34C}p*s*Y19ytWD?h$P)*!%jOCxBDyIP`t74A*ejor?HHUM98796fWE@S2)PYD zn;?J*#ZtuQ@0gnLneC!6m@LYko?H z0iK^C4T1AU6UWC}8O+O7qez#?*|bhyo`6*a26DU?^q|4XVU&;UK57VTG5qx)x1UY$ zjm^ObAN$F$k#vJih2LMhoVJL-j|e>N;7^U%5`D?SbQX5(+4`c}WR+bmiFUHK@1x4w z7?4LZ)y&iu_K0tMl)s@f?^9eXOzSG_L1W_Md>K;!+Slh}5y%LS?MzQh7!f*H+T0m9IzU4n^T+*E|GF^%H7V?-^ zs3LgJ&~eZL6PYVp8l5I_S7BC?ExQ=Vo39C>=XG9KJzKw-FVwzxq`uR5DznHOP&~p& zSy9$bSUW&Y+5&U_rkxNZEHbSOFU?j@WZeCFi*RE49D(_Y)G1I^k`BM_p4HuS`HG*8 zO9Zb}qWAgA2xsO8)HZx#5-_hoo2^GS(G*ojn$G>1zh z%8(7J$6>EgPMjMsxs6|5o+U2XR=W-BvfCi{W~WPnsL!;bxYZnz1~I&fs;V3pT*Hmk zi+Kp1?`(4cs&yy_R6~KAiIg$l!SHc~h1^t8E!s3c&R$PVg)jI!Sv9(e*g4EE{1OA+ ze-9W>RN-WiQlY;>c(x1Z@aXCZR$yO|`$IY+CHV{o8lKWNV@(Luo}HW7iL_d_Y(uAz zhwWci4znKN%ecr(8I8h!ol8RDDW-yZZ{~mS{3`D~AWpnFj6Z9_Xkt>^JU8x3q}=M{ zgAR%kvLh#Lv`<0f@}Qr8k&92h!@~0jQ5@OQ`4Lx7=lKUYP|3z4ZR%N6vaGFnG*_F1 zacK95+}GJ-Uv~!`0|Z~~${+|7Vs#;lXdtgDr?f4EHM)Q_#MK#J$W%L5fV-oxajqn* z-l{Svv0|A=QJAgIZ&E&@z!gd>&Pvz#_xJ<&A02BvHKixoGl9Kh<%HMuD?Dd%6&(&z z?_P5WKPyYY;d{_$?40p2Da>2vbQiDVwhX95fechj5#{ zDsCej=%IKGM^3V~y_0oZucuImm0IX@@6zI6r4`0ZO5OBZ6bCqTp$eOO2wIi3|hdb;ySN7sH<-zl{9%`N(S!QtM`;j2zwo3+&*1L!nD)_S$T#$xqW1NAIAnhSdR@Xpz_0aBp6~cwQ^q4ra27kEOhPz81PsOai-HY zE%=aw$0XX%%r`)O?|4cwl(Sx_79$h^Kw=)vNsy8zVf0(&D5N4*86ZkQbr73@ypceK zdMjgiitcm$ggPsUh@1H2r0JNS2J3Syf@0|$50KScp10@zTs%He9M63U&nT`Ld{xJb z_LdB#GgJxbKhv=<81!?`Er1XN26v8YKZHa(CG^ox<94V2kE@Qt3s8 z7Aik$AbDlR3tPi(J_-FQU4y2{Og0)zZP62?IVsVEFqzu(X_4s``s>F(fl@t&B+m> zJ#jb=6Ttifu+fAeNet)Vn&^wlE|5pZ)B%ec#C_I z0+s@{XNYsa7Zx3ah&R+xoR)oYwx|*7pplg9jkA2}y4{7)i_ z-U;s2OfX?=q&}o3E&0J`KyMy`Br;f`?lDCtg0%d$4D5%?mor{Yl{%L;TCCa@=XY`? zvsMi`Efjem8TNA7<0FzU3BTaNpiRa7_H2p+3j*q{0*}b6WnSp^R2Xndb94-)9m{98 z8ZY=aBm9gx?sjf3w@H0m^@;8CLVGAvtEQ&K#`(bmD~DYog9M!+7U)4+N&#dyQP6*Z|c6QJpL)?iRrvG z(7~4uJvW43ow@X=_MU8aQ@4_Hz_VWWJbp-BjFXp-Va1|%U};)?y3Y1kdAu35mzZfT zpLc<&7-=kBvJP)a)Pe&nVxYNu{d6ay%<(mVGzZb~Q_?Yhb)@q9?7wz7Ho4OXBZ6Qh z&^FLFUD%9PMx%~`ca5Yrc^0MDZHx7Zs|`gnXWa{EGgnS4R*`;gOu`b<=SW~)B|ajT zkpa)M7k?0|E201>O=I;i2&w{osl_fQWmVOk42GzNLJj%`FAd&B`OWYMCdPz3qB;LU=rf?%-AQu zsfkCFy$Vd2Lqnyyb7A1?xPeHu z4iU-_g$f52*e3`95>JL<2{@4sp*zmF>1za38`wst`t8*ks3E1M zL+YfY)zfA;BF%puOVdjcT#7E7-c3h01op@yBvqA3XY_MFY$GMV5PkkUn>YuT!!dF5qb44IMJ}=T8VfJykI3NyeLa)g5CZHA z)Sln&xDzr_o?Yve`Sj_NIbS22O*;k$R0OCFnVA7t49PouNdiD-*N zrY747T%-jfe*C75KYNZ!6^!s+SkSt_IK(Y)c#Qil4w^i^ zyX)L$QQ>=Yw)E|1DwP3CMM2`sCIeCQK0X}%io8rwsYVR*2P?rlU5%#KyQ2YNHhRqT zv$M#0=7RJi(1d09q0_5oNu$YPVgo6G^sK<{Ip0lf<(1HOqruac`Th#nTCZame+?3e z?y|k4j<0?!tV*cCi&hHCz%yJxT1Zg+~n0^dw|dgJ{%$`CRHs&ib_-^f=IB34*Y7-aF|bx~b0 zl2K?qpQSUZsgTT_anl+I#k?_ZVxRjMMM?2gH5vbT`Oo0#2UVB;In6o5(X7#9x;k#& zjjua_7%&u%(WWfidXDXK=kO@-ct<|}9x!bbta6%f)l5|rbTnc(L?#iM;4b?7H1WN+ zkb<#s*Jfz|TE_Dvw+PzL@NULgw3FTpN{^x>xU-df_h3u!g|xHSE8IrpUSP&4(yo$Z zVisTR#oHI&s!_huUd>I247yLn@h%N@!Yjypo@n5dCngkK@NO1y3zVeza<%4 z@%wy#t+96HS$8M~60+nlaQ;#{LFoYgA%-4f}z zblbzj`s}K0%D8*h)vjIFoEQ>l~=*V-nh()mLW{`==T>w{qTd5UAM>@YfKV^{S=Es>kD%Z3`q1Dd%GX zvDlk#veTsckoZOT(Gavc?hF+2lt)nS>8KBrFq zma^y?ckq|@dHk6;6~DeM=L!hH13y5hY-=g{^>r&^v>7s}le{1vMP6h}k=Z(%s_D@f0TIe>`Wg3c5s_=M`+%(5}K7VC|3Pk-qEMV?#hIR=6 zuI}1ksO>6H+b9vq<`@yB<7@_umKA4A`qpQ73fLNP1B?iXeQyzNkGx--JDc3gnmc)N zaV?PhC})SwH^-3+8p^AfSt@%XVk-awz5M_tfh7Lke8045sehYrkA^x1HU}mpsD*ZL)^N}$+joN$RR*SN z&q<8(U(+`Kca{53wfmp@|GopdfIlhVqxcA#hh{Tc_@u1aQsSblk;Ut*kqjuEj8Ryo zAbmD{*oWEL*~pSwUo-G5i9c9J16F*<1f$nZ9l4bJsHhlkA^zQCjd#g6Z%YnDp}J}G%r1?F1-<&BGT_?h(K`BRvku$a~A&Can`xJwAQf`U;rwqTbTmQur%^-C~*o@wc#6__)72_X}>@`-5Dzp2-feK Date: Fri, 31 Dec 2021 02:04:35 -0800 Subject: [PATCH 23/27] cleanup seems to be working --- .../Repositories/FirestoreService.swift | 14 ++ nirvana-ios/Views/Convos/ConvoViewModel.swift | 140 ++++++++++-------- 2 files changed, 89 insertions(+), 65 deletions(-) diff --git a/nirvana-ios/Repositories/FirestoreService.swift b/nirvana-ios/Repositories/FirestoreService.swift index a3ed05c..59e1aad 100644 --- a/nirvana-ios/Repositories/FirestoreService.swift +++ b/nirvana-ios/Repositories/FirestoreService.swift @@ -188,6 +188,20 @@ 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) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 8852075..8ebc4d9 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -18,11 +18,11 @@ class ConvoViewModel: NSObject, ObservableObject { var convosListener: ListenerRegistration? = nil let agoraService = AgoraService() - var agoraKit: AgoraRtcEngineKit? + // 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] = [] - private var currConvo: Convo? = nil // local cache for updating func isInCall() -> Bool { return self.selectedConvoId != nil @@ -175,16 +175,12 @@ class ConvoViewModel: NSObject, ObservableObject { self.leaveConvo() } - // add this convo for the delegate use - self.currConvo = convo - // update ui to select this one self.selectedConvoId = convo.id - self.initializeAgoraEngine() // give delegate up to date object - 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) } @@ -211,38 +207,81 @@ class ConvoViewModel: NSObject, ObservableObject { return } - self.currConvo = convo - // 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.initializeAgoraEngine() - 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) - // if I am the second person in the room, then end the convo + let userId = AuthSessionStore.getCurrentUserId() - self.selectedConvoId = nil - self.currConvo = nil + if userId == nil { + print("not authenticated...can't join channel") + return + } + if self.selectedConvoId == nil { + print("not in a convo currently!") + return + } + + self.firestoreService.getConvo(channelName: self.selectedConvoId!) {convo in + if convo == nil { + print("no such convo found") + return + } + + var updatedConvo = convo + + self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in + print(res) + + switch res { + case .success: + //update convo in database to show that I left...if I am the last person, also close out the channel + + let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in + return arrUserId != userId + } + + updatedConvo!.users = updatedUsers + + // if I am the last one in the convo, then end the convo + if updatedConvo!.users.count == 1 && updatedConvo!.users[0] == userId { + updatedConvo!.state = .complete + updatedConvo!.endedTimestamp = Date() + } + + self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in + self?.selectedConvoId = nil + + print("left convo officially in our db as well") + } + case .error(let error): + print(error) + default: + return + } + } + } + } + + func destroyInstance() { AgoraRtcEngineKit.destroy() } } extension ConvoViewModel { - func initializeAgoraEngine() { - // TODO: put app id in environment variables - agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) - } func playJoinAudioEffect(engine: AgoraRtcEngineKit) { print("playing JOINED audio sound") @@ -305,27 +344,35 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { // TODO: leave channel if it's just me - if let userId = AuthSessionStore.getCurrentUserId() { - if self.currConvo == nil { + 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 } - // set my user status to in convo - self.firestoreService.updateUserStatus(userId: userId, userStatus: .inConvo) {[weak self] res in + 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 - self?.currConvo!.users.append(userId) - self?.currConvo!.state = .active + updatedConvo!.users.append(userId!) + updatedConvo!.state = .active - self?.firestoreService.updateConvo(convo: (self?.currConvo)!) {[weak self] res in + self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in print(res) - - + } case .error(let error): print(error) @@ -342,44 +389,7 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { // sound effect upon leaving self.playLeaveAudioEffect(engine: engine) - if self.currConvo == nil { - print("no such convo found") - return - } - - if let userId = AuthSessionStore.getCurrentUserId() { - // set my user status to online - self.firestoreService.updateUserStatus(userId: userId, userStatus: .online) {[weak self] res in - print(res) - - switch res { - case .success: - //update convo in database to show that I left...if I am the last person, also close out the channel - - let updatedUsers: [String] = (self?.currConvo!.users.filter{ arrUserId in - return arrUserId != userId - })! - - self?.currConvo!.users = updatedUsers - - // if I am the last one in the convo, then end the convo - if self?.currConvo!.users.count == 1 && self?.currConvo!.users[0] == userId { - self?.currConvo!.state = .complete - self?.currConvo!.endedTimestamp = Date() - } - - self?.firestoreService.updateConvo(convo: (self?.currConvo)!) {[weak self] res in - self?.selectedConvoId = nil - self?.currConvo = nil - print("left convo officially in our db as well") - } - case .error(let error): - print(error) - default: - return - } - } - } + print("properties in leave channel: \(self)") } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { From 9ef95b344542182ef2561771022a35212654ee65 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 02:14:39 -0800 Subject: [PATCH 24/27] Update ConvoViewModel.swift --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 8ebc4d9..d05731d 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -86,7 +86,7 @@ class ConvoViewModel: NSObject, ObservableObject { continue } - // TODO: if convo receiver is me, then join in + // TODO: if convo receiver is me and I'm not in a call, then join in if convo!.receiverUserId == userId && !(self?.isInCall())! { self?.joinConvo(convo: convo!) } @@ -250,17 +250,18 @@ class ConvoViewModel: NSObject, ObservableObject { case .success: //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 + if updatedConvo!.users.count == 1 && updatedConvo!.users[0] == userId { + updatedConvo!.state = .complete + updatedConvo!.endedTimestamp = Date() + } + let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in return arrUserId != userId } updatedConvo!.users = updatedUsers - // if I am the last one in the convo, then end the convo - if updatedConvo!.users.count == 1 && updatedConvo!.users[0] == userId { - updatedConvo!.state = .complete - updatedConvo!.endedTimestamp = Date() - } self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in self?.selectedConvoId = nil From ca586f8331e02f4e7a2e99ee194f28beb6621c77 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 02:36:14 -0800 Subject: [PATCH 25/27] some cleaning up --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index d05731d..eb5d9b7 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -86,7 +86,7 @@ class ConvoViewModel: NSObject, ObservableObject { continue } - // TODO: if convo receiver is me and I'm not in a call, then join in + // TODO: 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())! { self?.joinConvo(convo: convo!) } @@ -235,44 +235,44 @@ class ConvoViewModel: NSObject, ObservableObject { return } - self.firestoreService.getConvo(channelName: self.selectedConvoId!) {convo in + 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 - self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in - print(res) + // 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() + } + + 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 - switch res { - case .success: - //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 - if updatedConvo!.users.count == 1 && updatedConvo!.users[0] == userId { - updatedConvo!.state = .complete - updatedConvo!.endedTimestamp = 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") - } - case .error(let error): - print(error) - default: - return - } + print("left convo officially in our db as well") } } } @@ -389,8 +389,6 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { // sound effect upon leaving self.playLeaveAudioEffect(engine: engine) - - print("properties in leave channel: \(self)") } func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { From 70c2ad86215daf30ddaa880851b26e42634a5095 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 02:54:11 -0800 Subject: [PATCH 26/27] brute force database to end call --- nirvana-ios/Models/Convo.swift | 1 + nirvana-ios/Views/Convos/ConvoViewModel.swift | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift index a054175..cd75d6e 100644 --- a/nirvana-ios/Models/Convo.swift +++ b/nirvana-ios/Models/Convo.swift @@ -30,6 +30,7 @@ struct Convo: Identifiable, Codable { @ServerTimestamp var startedTimestamp: Date? var endedTimestamp: Date? + var receiverEndedTimestamp: Date? } //struct DetailConvo: Convo { diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index eb5d9b7..81aa34b 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -87,7 +87,8 @@ class ConvoViewModel: NSObject, ObservableObject { } // TODO: 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())! { + if convo!.receiverUserId == userId && !(self?.isInCall())! + && convo!.receiverEndedTimestamp == nil { self?.joinConvo(convo: convo!) } @@ -261,6 +262,9 @@ class ConvoViewModel: NSObject, ObservableObject { 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 From 3e4a3356318a9ae5d037004074441eab76ad1998 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 31 Dec 2021 03:06:15 -0800 Subject: [PATCH 27/27] handles both ways now pretty smooth for two people but don't know about three --- nirvana-ios/Views/Convos/ConvoViewModel.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 81aa34b..44da5ed 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -86,12 +86,17 @@ class ConvoViewModel: NSObject, ObservableObject { continue } - // TODO: if convo receiver is me and I'm not in a call or this call already, then join in + // 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!) }