Merge pull request #2 from talksik/show-online-users

Show online users
This commit is contained in:
Arjun Patel
2021-12-28 16:50:35 -08:00
committed by GitHub
7 changed files with 174 additions and 40 deletions
+8
View File
@@ -10,6 +10,12 @@ import Firebase
import FirebaseFirestore import FirebaseFirestore
import FirebaseFirestoreSwift import FirebaseFirestoreSwift
enum UserStatus: String, Codable {
case online
case offline
case background
}
struct User: Identifiable, Codable { struct User: Identifiable, Codable {
@DocumentID var id: String? @DocumentID var id: String?
var nickname: String? var nickname: String?
@@ -18,6 +24,8 @@ struct User: Identifiable, Codable {
var avatar:String? var avatar:String?
var deviceToken: String? var deviceToken: String?
var userStatus: UserStatus?
@ServerTimestamp var lastLoggedInTimestamp: Date? @ServerTimestamp var lastLoggedInTimestamp: Date?
@ServerTimestamp var createdTimestamp: Date? @ServerTimestamp var createdTimestamp: Date?
@@ -155,7 +155,10 @@ class FirestoreService {
completion(ServiceState.error(ServiceError(description: error.localizedDescription))) completion(ServiceState.error(ServiceError(description: error.localizedDescription)))
} }
} }
}
// updates to user
extension FirestoreService {
func updateUserDeviceToken(userId: String, deviceToken: String, completion: @escaping((_ state: ServiceState) -> ())) { func updateUserDeviceToken(userId: String, deviceToken: String, completion: @escaping((_ state: ServiceState) -> ())) {
do { do {
if userId != nil { if userId != nil {
@@ -170,4 +173,13 @@ class 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)
completion(ServiceState.success("Updated user status"))
} catch {
print("error in updating user \(error.localizedDescription)")
completion(ServiceState.error(ServiceError(description: error.localizedDescription)))
}
}
} }
+79 -22
View File
@@ -42,6 +42,8 @@ final class AuthSessionStore: ObservableObject, SessionStore {
@Published var relevantUsersDict: [String: User] = [:] // all cached/snapshotted users from db for app use @Published var relevantUsersDict: [String: User] = [:] // all cached/snapshotted users from db for app use
@Published var relevantMessagesByUserDict: [String: [Message]] = [:] // note, this is all messages related to me @Published var relevantMessagesByUserDict: [String: [Message]] = [:] // note, this is all messages related to me
private var friendsListeners: [ListenerRegistration] = []
private var dataListeners: [ListenerRegistration] = [] private var dataListeners: [ListenerRegistration] = []
private var listenersActive = false // false on app/processes killed private var listenersActive = false // false on app/processes killed
@@ -126,7 +128,7 @@ final class AuthSessionStore: ObservableObject, SessionStore {
do { do {
try Auth.auth().signOut() try Auth.auth().signOut()
self.deinitDataListeners() self.deinitAllDataListeners()
} catch let signOutError as NSError { } catch let signOutError as NSError {
print("Error signing out: %@", signOutError) print("Error signing out: %@", signOutError)
} }
@@ -243,6 +245,9 @@ extension AuthSessionStore {
self.userFriendsDict.removeAll() self.userFriendsDict.removeAll()
self.friendsArr.removeAll() // updating to keep the friends in order self.friendsArr.removeAll() // updating to keep the friends in order
// clear the listeners for the the previous set of users
self.deinitFriendListeners()
for document in querySnapshot!.documents { for document in querySnapshot!.documents {
let userFriend:UserFriends? = try? document.data(as: UserFriends.self) let userFriend:UserFriends? = try? document.data(as: UserFriends.self)
@@ -252,37 +257,54 @@ extension AuthSessionStore {
self.userFriendsDict[userFriend!.friendId] = userFriend! self.userFriendsDict[userFriend!.friendId] = userFriend!
// get user and initialize this user in dict // making sure that I clear the inbox if this user came from the inbox...
self.db.collection("users").document(userFriend!.friendId).getDocument { (document, error) in // could be an accepted or rejected user
if let document = document, document.exists { if self.inboxUsersArr.contains(userFriend!.friendId) {
let returnedUser = try? document.data(as: User.self) self.inboxUsersArr = self.inboxUsersArr.filter {inboxUserId in
return inboxUserId != userFriend!.friendId
}
}
DispatchQueue.main.async { // only get friend's info if is an active friend
if returnedUser != nil { if !userFriend!.isActive {
self.relevantUsersDict[userFriend!.friendId] = returnedUser! continue
}
// making sure that I clear the inbox if this user came from the inbox...regardless of // create a listener for this specific user
// whether I accepted or rejected them let currFriendListener = self.db.collection("users").document(userFriend!.friendId)
if self.inboxUsersArr.contains(userFriend!.friendId) { .addSnapshotListener {documentSnapshot, error in
self.inboxUsersArr = self.inboxUsersArr.filter {inboxUserId in guard let document = documentSnapshot else {
return inboxUserId != userFriend!.friendId print("Error fetching friend's data: \(error!)")
} return
} }
if userFriend!.isActive { guard let data = document.data() else {
print("friend realtime data was empty.")
return
}
let updatedReturnedUser = try? document.data(as: User.self)
if updatedReturnedUser != nil {
DispatchQueue.main.async {
self.relevantUsersDict[userFriend!.friendId] = updatedReturnedUser!
// update current friendsArr if the friend is already there
if let indexFriend = self.friendsArr.firstIndex(of: userFriend!.friendId) {
print("friend already in the friends arr, just needed to update the relevantusersdict")
} else { // new friend not in array
self.friendsArr.append(userFriend!.friendId) self.friendsArr.append(userFriend!.friendId)
} }
self.objectWillChange.send() self.objectWillChange.send()
print("added this user to the array of users for user's circle\(returnedUser?.nickname)") print("added this user to the array of users for user's circle\(updatedReturnedUser?.nickname)")
} }
} }
} else {
print("user doesn't exist from user friend relationship")
// should not happen
} }
}
// adding to list of current friend listeners
self.friendsListeners.append(currFriendListener)
} }
} }
@@ -401,13 +423,48 @@ extension AuthSessionStore {
} }
func deinitDataListeners() { func deinitFriendListeners() {
for listener in self.friendsListeners {
listener.remove()
}
}
func deinitAllDataListeners() {
// get rid of the main listeners
for listener in self.dataListeners { for listener in self.dataListeners {
listener.remove() listener.remove()
} }
// get rid of the inner listeners
self.deinitFriendListeners()
print("deactivated listeners!") print("deactivated listeners!")
self.listenersActive = false self.listenersActive = false
} }
} }
// manage whether user is online or not
extension AuthSessionStore {
func updateUserStatus(userStatus: UserStatus) {
// if user is authenticated
if self.sessionState == .isAuthenticated {
// if user already has the status that we want to update to, no need to update
// remember that the user is updated realtime so we have the newest user data
if self.user?.userStatus == userStatus {
// do nothing...already have this status
print("user already has status: \(userStatus)")
}
else {
if let uid = self.getCurrentUserId() {
self.firestoreService.updateUserStatus(userId: uid, userStatus: userStatus) {res in
print(res)
}
}
}
}
print("not authenticated can't change user status")
}
}
@@ -80,8 +80,15 @@ struct CircleGridView: View {
.blur(radius: 8) .blur(radius: 8)
.cornerRadius(100) .cornerRadius(100)
// check if the last message in the conversation between me and my friend was me talking or him // this friend is online
if self.haveNewMessageFromFriend(friendDbId: friendId) { // him talking if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.green)
.font(.title2)
.padding(5)
}
else if self.haveNewMessageFromFriend(friendDbId: friendId) { // check if the last message in the conversation between me and my friend was me talking or him
Image(systemName: "arrow.down.left.circle.fill") Image(systemName: "arrow.down.left.circle.fill")
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
.font(.title2) .font(.title2)
@@ -370,6 +377,8 @@ extension CircleGridView {
extension CircleGridView { extension CircleGridView {
// listening to messages // listening to messages
private func handleTap(gridItemIndex: Int, friendId: String) { private func handleTap(gridItemIndex: Int, friendId: String) {
// TODO: if there is a message, then listen to it
// then after that, check if friend is online, and if so, then connect with him/her if they are free/not in another call
print("tap gesture activated") print("tap gesture activated")
// clearing the player to make room for this friend's convo or to deselect this user // clearing the player to make room for this friend's convo or to deselect this user
@@ -65,7 +65,6 @@ struct CircleNavigationView: View {
Label("log out", systemImage: "rectangle.portrait.and.arrow.right") Label("log out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(NirvanaColor.teal) .foregroundColor(NirvanaColor.teal)
} }
} label: { } label: {
if self.authSessionStore.user?.avatar == nil { if self.authSessionStore.user?.avatar == nil {
Image("Artboards_Diversity_Avatars_by_Netguru-1") Image("Artboards_Diversity_Avatars_by_Netguru-1")
@@ -75,7 +74,19 @@ struct CircleNavigationView: View {
.blur(radius: 5) .blur(radius: 5)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.clipShape(Circle()) .clipShape(Circle())
.padding(5) .overlay(alignment: .bottomTrailing) {
// user status
switch self.authSessionStore.user?.userStatus {
case .online:
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
case .offline:
Circle()
default:
Circle()
}
}
} else { } else {
Image((self.authSessionStore.user?.avatar)!) Image((self.authSessionStore.user?.avatar)!)
.resizable() .resizable()
@@ -83,8 +94,20 @@ struct CircleNavigationView: View {
.background(self.innerCircleVM.isRecording ? Color.orange : NirvanaColor.teal.opacity(0.5)) .background(self.innerCircleVM.isRecording ? Color.orange : NirvanaColor.teal.opacity(0.5))
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.clipShape(Circle()) .clipShape(Circle())
.padding(5)
.shadow(radius: 10) .shadow(radius: 10)
.overlay(alignment: .topTrailing) {
// user status
switch self.authSessionStore.user?.userStatus {
case .online:
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
case .offline:
Circle()
default:
Circle()
}
}
} }
} }
@@ -13,6 +13,7 @@ struct InnerCircleView: View {
@EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var authSessionStore: AuthSessionStore
@EnvironmentObject var navigationStack: NavigationStack @EnvironmentObject var navigationStack: NavigationStack
@State var selectedFriendIndex: String? = nil @State var selectedFriendIndex: String? = nil
let universalSize = UIScreen.main.bounds let universalSize = UIScreen.main.bounds
@@ -91,11 +92,10 @@ struct InnerCircleView: View {
} }
// header // header
VStack(alignment: .leading) { ZStack(alignment: .topLeading) {
Color.clear
CircleNavigationView(alertActive: self.$alertActive, alertText: self.$alertText, alertSubtext: self.$alertSubtext).environmentObject(innerCircleVM) CircleNavigationView(alertActive: self.$alertActive, alertText: self.$alertText, alertSubtext: self.$alertSubtext).environmentObject(innerCircleVM)
Spacer()
} }
CircleFooterView(selectedFriendIndex: self.$selectedFriendIndex).environmentObject(innerCircleVM) CircleFooterView(selectedFriendIndex: self.$selectedFriendIndex).environmentObject(innerCircleVM)
@@ -134,17 +134,22 @@ struct InnerCircleView: View {
} }
.onAppear { .onAppear {
// activate 3 data listeners once for authsessionstore/usermanager if not already called, but authsessionstore will handle that // activate 3 data listeners once for authsessionstore/usermanager if not already called, but authsessionstore will handle that
// TODO: can/should do this on init of view model
self.authSessionStore.activateMainDataListeners() self.authSessionStore.activateMainDataListeners()
// set up push notifications and such + save up to date device token // set up push notifications and such + save up to date device token
// TODO: this is firing too often? // TODO: this is firing too often?
self.innerCircleVM.setUpPushNotifications() self.innerCircleVM.setUpPushNotifications()
// set status of user to online
self.authSessionStore.updateUserStatus(userStatus: .online)
} }
// .onDisappear { // .onDisappear {
// print("deiniting data listeners, but current data should still be cached!") // print("deiniting data listeners, but current data should still be cached!")
// //
// // deactivate all data listeners // // deactivate all data listeners
// self.authSessionStore.deinitDataListeners() // self.authSessionStore.deinitAllDataListeners()
// } // }
} }
} }
+27 -7
View File
@@ -19,16 +19,36 @@ enum NavigationPages {
struct RouterView: View { struct RouterView: View {
@EnvironmentObject var authSessionStore: AuthSessionStore @EnvironmentObject var authSessionStore: AuthSessionStore
@EnvironmentObject var navigationStack: NavigationStack @EnvironmentObject var navigationStack: NavigationStack
@Environment(\.scenePhase) var scenePhase
var body: some View { var body: some View {
switch self.authSessionStore.sessionState { ZStack {
case SessionState.notCheckedYet: switch self.authSessionStore.sessionState {
SplashView() case SessionState.notCheckedYet:
case SessionState.isAuthenticated: SplashView()
InnerCircleView() case SessionState.isAuthenticated:
case SessionState.isLoggedOut: InnerCircleView()
WelcomeView() 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)
}
}
} }
} }