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..