diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index b6152cb..dcb4690 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 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 */; }; + 8909199E278155920025F4BB /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8909199D278155920025F4BB /* UserStatusView.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 */; }; @@ -102,6 +103,7 @@ 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 = ""; }; + 8909199D278155920025F4BB /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.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 = ""; }; @@ -316,6 +318,7 @@ 891A15FE27656F9200B26659 /* HeaderView.swift */, 89288ECA27675B9F00E962C4 /* OnboardingTemplateView.swift */, 89BCABEC276D2B05009E9B10 /* WavesGlassBackgroundView.swift */, + 8909199D278155920025F4BB /* UserStatusView.swift */, ); path = Templates; sourceTree = ""; @@ -656,6 +659,7 @@ 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */, 896CE7022770892F00DB474E /* CircleGridView.swift in Sources */, 89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */, + 8909199E278155920025F4BB /* UserStatusView.swift in Sources */, 89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */, 89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */, 89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */, diff --git a/nirvana-ios/Models/Convo.swift b/nirvana-ios/Models/Convo.swift index cd75d6e..5b8cd99 100644 --- a/nirvana-ios/Models/Convo.swift +++ b/nirvana-ios/Models/Convo.swift @@ -30,7 +30,7 @@ struct Convo: Identifiable, Codable { @ServerTimestamp var startedTimestamp: Date? var endedTimestamp: Date? - var receiverEndedTimestamp: Date? + var secondToLastUserEndedTimestamp: Date? } //struct DetailConvo: Convo { diff --git a/nirvana-ios/Services/AuthSessionStore.swift b/nirvana-ios/Services/AuthSessionStore.swift index cbc2740..73bf2df 100644 --- a/nirvana-ios/Services/AuthSessionStore.swift +++ b/nirvana-ios/Services/AuthSessionStore.swift @@ -224,9 +224,11 @@ extension AuthSessionStore { // TODO: THIS WON'T UPDATE VIEW since user is a reference type...can manually publish...research and learn more self.firestoreService.getUserRealtime(userId: currUserId!) {[weak self] realtimeUpdatedUser in if realtimeUpdatedUser != nil { - print("up to date user: \(realtimeUpdatedUser)") + print("up to date user") self?.user = realtimeUpdatedUser self?.objectWillChange.send() + + self?.relevantUsersDict[currUserId!] = realtimeUpdatedUser } } diff --git a/nirvana-ios/Views/Convos/ConvoFooterView.swift b/nirvana-ios/Views/Convos/ConvoFooterView.swift index 98b645b..8b07539 100644 --- a/nirvana-ios/Views/Convos/ConvoFooterView.swift +++ b/nirvana-ios/Views/Convos/ConvoFooterView.swift @@ -9,89 +9,95 @@ import SwiftUI struct ConvoFooterView: View { @EnvironmentObject var convoVM: ConvoViewModel + @EnvironmentObject var authSessionStore: AuthSessionStore @State private var convoRelativeTime = "started 20 minutes ago" + @State private var selectedConvo: Convo? = nil var body: some View { + ZStack(alignment:.bottomTrailing) { VStack(alignment:.center) { Spacer() HStack { - ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35)) - .padding(.leading, 20) - .padding(.trailing, 10) - - - // meta data of convo - VStack (alignment: .leading) { - Text("kev, sarth, and 5 others") - .font(.footnote) - .foregroundColor(NirvanaColor.light) + if self.selectedConvo != nil { + let filteredUsers = self.authSessionStore.relevantUsersDict.filter { + return self.selectedConvo!.users.contains($0.key) + } + let convoUsers: [User] = Array(filteredUsers.values) - 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) + ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35), users: convoUsers) + .padding(.leading, 20) + .padding(.trailing, 10) + + // meta data of convo + VStack (alignment: .leading) { + ConvoUsernames(users: convoUsers) + + 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) } - Text(self.convoRelativeTime) - .font(.caption2) - .foregroundColor(.gray) - } - - Spacer() - - // disconnect button - Button { - print("showing more info about the chat") + Spacer() - // show a sheet of the members in the call and add them if I want + // disconnect button + Button { + print("showing more info about the chat") + + // show a sheet of the members in the call and add them if I want + + } label: { + Label("attendees", systemImage: "person.2.circle.fill") + .labelStyle(.iconOnly) + .foregroundColor(NirvanaColor.teal) + .font(.title2) + .padding(.trailing, 5) + } - } label: { - Label("attendees", systemImage: "person.2.circle.fill") - .labelStyle(.iconOnly) - .foregroundColor(NirvanaColor.teal) - .font(.title2) - .padding(.trailing, 5) + // disconnect button + Button { + print("disconnecting user now ") + + self.convoVM.leaveConvo() + + // show success or failure toast + } label: { + Label("Call", systemImage: "powerplug.fill") + .labelStyle(.iconOnly) + .foregroundColor(Color.orange) + .font(.title2) + .padding(.trailing, 5) + } } - - // disconnect button - Button { - print("disconnecting user now ") - - self.convoVM.leaveConvo() - - // show success or failure toast - } label: { - Label("Call", systemImage: "powerplug.fill") - .labelStyle(.iconOnly) - .foregroundColor(Color.orange) - .font(.title2) - .padding(.trailing, 5) - } - } .frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle .background(Color.white.opacity(0.5)) @@ -105,17 +111,25 @@ struct ConvoFooterView: View { x:0, y:self.convoVM.selectedConvoId == nil ? 150 : 0 ) - .onChange(of: self.convoVM.selectedConvoId) {newConvo in + .onReceive(self.convoVM.$relevantConvos) {newConvos in // update meta data based on which convo was selected + if self.convoVM.selectedConvoId == nil { + print("nothing to show as there is no convo selected") + return + } + // reset the selected options to start fresh -// self.convoRelativeTime = "" + self.convoRelativeTime = "" + + self.selectedConvo = newConvos.first {convo in + convo.id == self.convoVM.selectedConvoId + } // get relative start time of convo -// let formatter = RelativeDateTimeFormatter() -// formatter.unitsStyle = .full -// -// let relativeDate = formatter.localizedString(for: ?.sentTimestamp ?? Date(), relativeTo: Date()) + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + self.convoRelativeTime = formatter.localizedString(for: self.selectedConvo?.startedTimestamp ?? Date(), relativeTo: Date()) } } } @@ -125,3 +139,14 @@ struct ConvoFooterView_Previews: PreviewProvider { ConvoFooterView() } } + +struct ConvoUsernames: View { + var users: [User] + + var body: some View { + let userNames = self.users.map{$0.nickname ?? ""}.joined(separator: ",") + Text(userNames) + .font(.footnote) + .foregroundColor(NirvanaColor.light) + } +} diff --git a/nirvana-ios/Views/Convos/ConvoViewModel.swift b/nirvana-ios/Views/Convos/ConvoViewModel.swift index 44da5ed..fa8ed49 100644 --- a/nirvana-ios/Views/Convos/ConvoViewModel.swift +++ b/nirvana-ios/Views/Convos/ConvoViewModel.swift @@ -11,7 +11,7 @@ import Firebase class ConvoViewModel: NSObject, ObservableObject { @Published var selectedConvoId:String? = nil - @Published var connectionState: AgoraConnectionStateType? = nil + @Published var connectionState: AgoraConnectionStateType? let firestoreService = FirestoreService() private var db = Firestore.firestore() @@ -19,7 +19,7 @@ class ConvoViewModel: NSObject, ObservableObject { let agoraService = AgoraService() // TODO: put the value in environment variables - lazy var agoraKit: AgoraRtcEngineKit? = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) + var agoraKit: AgoraRtcEngineKit? private var allConvos: [Convo] = [] @Published var relevantConvos: [Convo] = [] @@ -28,15 +28,22 @@ class ConvoViewModel: NSObject, ObservableObject { return self.selectedConvoId != nil } - static var relevancyAcceptance = 0.6 + static var relevancyAcceptance = 0.5 + + func initializeAgoraEngine() { + self.agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) + } override init() { super.init() + initializeAgoraEngine() + // start process of data collection let userId = AuthSessionStore.getCurrentUserId() if userId == nil { print("no authenticated user") + return } // get all active userfriends @@ -88,13 +95,19 @@ class ConvoViewModel: NSObject, ObservableObject { // 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 { + && convo!.secondToLastUserEndedTimestamp == nil { + print("I am the direct receiver...joining convo") 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!) { + else if convo!.secondToLastUserEndedTimestamp != nil && (self?.isInCall())! && convo!.users.contains(userId!) { + print("I am the last one now... ending the convo") self?.leaveConvo() + } //also if I am added after a receiver was added and I am not already in a convo, then join the call + else if convo!.users.contains(userId!) && !(self?.isInCall())! { + // should be free or online and not in another call unless it's a race condition + print("I was added onto the call because I was free...joining automatically") + self?.joinConvo(convo: convo!) } self?.allConvos.append(convo!) @@ -111,10 +124,10 @@ class ConvoViewModel: NSObject, ObservableObject { } } - let relevancyScore = Double(relevancyCount / convo.users.count) + let relevancyScore = Double(relevancyCount) / Double(convo.users.count) print("relevancy score: \(relevancyScore)") - if relevancyScore > Self.relevancyAcceptance { + if relevancyScore > Self.relevancyAcceptance && convo.users.count > 1 { //offchance that it's just the one person in the convo right now, don't want to see it until there are at least 2 people print("this convo counts as relevant!!!") return true } @@ -126,15 +139,12 @@ 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") - AgoraRtcEngineKit.destroy() + self.leaveConvo(destroyInstance: true) - self.leaveConvo() + // deinit the firestore listener + self.convosListener?.remove() } /** @@ -175,7 +185,8 @@ class ConvoViewModel: NSObject, ObservableObject { } } - func joinConvo(convo: Convo) { + /* internal control to join a convo */ + private func joinConvo(convo: Convo) { // ensure that this user leaves all other channels if self.isInCall() { self.leaveConvo() @@ -186,25 +197,19 @@ class ConvoViewModel: NSObject, ObservableObject { self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) // finally join channel - self.agoraKit?.delegate = self self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil) } - - + /** Allow user to join an active convo */ - func joinConvo() { + func joinConvo(convoId: 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 - if self.isInCall() { - print("can't join another call, already in one") - return - } + // ensure that this user leaves all other channels let convo = self.relevantConvos.first {convo in - convo.id == self.selectedConvoId + convo.id == convoId } if convo == nil { @@ -215,20 +220,44 @@ class ConvoViewModel: NSObject, ObservableObject { // TODO: if the convo has more than 10 people, stop user from joining...that's too expensive... + self.selectedConvoId = convoId let convoAgoraToken:String = convo!.agoraToken let channelName:String = convo!.id! + print("joining convo: \(channelName)") + 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) + func addThirdPartyToConvo(friendId: String) { + // recheck if this other friend that I am adding is online/free + if !self.isInCall() { + print("not in a call, can't invite/force someone else...must have a selected convo") + return + } + + // get the currconvo details + var convo = self.relevantConvos.first {convo in + convo.id == self.selectedConvoId + } + if convo == nil { + print("convo not available") + return + } + + // update convo users array with this other friend + convo!.users.append(friendId) + + self.firestoreService.updateConvo(convo: convo!) {[weak self] res in + print("updated with new friend added in the convo") + } + } + + // anyone can leave at any time + func leaveConvo(destroyInstance: Bool = false) { let userId = AuthSessionStore.getCurrentUserId() if userId == nil { @@ -241,6 +270,8 @@ class ConvoViewModel: NSObject, ObservableObject { return } + agoraKit?.leaveChannel(nil) + self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in print(res) @@ -269,7 +300,7 @@ class ConvoViewModel: NSObject, ObservableObject { 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() + updatedConvo!.secondToLastUserEndedTimestamp = Date() } let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in @@ -282,13 +313,19 @@ class ConvoViewModel: NSObject, ObservableObject { self?.selectedConvoId = nil print("left convo officially in our db as well") + + + // deinit the agora engine + if destroyInstance { + self?.destroyInstance() + } } } } func destroyInstance() { AgoraRtcEngineKit.destroy() - } + } } extension ConvoViewModel { @@ -377,7 +414,10 @@ extension ConvoViewModel: AgoraRtcEngineDelegate { case .success: // update convo in database users array to let them know I am in now - updatedConvo!.users.append(userId!) + // only add if I am not already in the users arr...if I was a third party, I should be in there already + if !updatedConvo!.users.contains(userId!) { + updatedConvo!.users.append(userId!) + } updatedConvo!.state = .active self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in diff --git a/nirvana-ios/Views/Convos/ConvosView.swift b/nirvana-ios/Views/Convos/ConvosView.swift index d5dff04..67ddb11 100644 --- a/nirvana-ios/Views/Convos/ConvosView.swift +++ b/nirvana-ios/Views/Convos/ConvosView.swift @@ -7,67 +7,70 @@ import SwiftUI -struct ConvosView: View { - @EnvironmentObject var vm: ConvoViewModel - @State var animate = false - - var body: some View { - ScrollView([.horizontal]) { - HStack { - ForEach(0..