Merge pull request #1 from talksik/connecting-data-to-circle-big-operation

Connecting data to circle big operation
This commit is contained in:
Arjun Patel
2021-12-20 19:03:13 -08:00
committed by GitHub
16 changed files with 910 additions and 469 deletions
+36 -4
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
8904DEC8277055E90071E6CA /* OnboardingTrioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8904DEC7277055E90071E6CA /* OnboardingTrioView.swift */; };
89110714277166D6005797FA /* UserFriends.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89110713277166D6005797FA /* UserFriends.swift */; };
891A15E127653A7000B26659 /* nirvana_iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891A15E027653A7000B26659 /* nirvana_iosApp.swift */; };
891A15E327653A7000B26659 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891A15E227653A7000B26659 /* ContentView.swift */; };
891A15E527653A7100B26659 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 891A15E427653A7100B26659 /* Assets.xcassets */; };
@@ -29,6 +30,12 @@
894E80BE276ED7A700624B72 /* avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894E80BD276ED7A700624B72 /* avatars.swift */; };
894E80C1276EFD0500624B72 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894E80C0276EFD0400624B72 /* AudioPlayerView.swift */; };
894E80C327702E5900624B72 /* AddBasicInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894E80C227702E5800624B72 /* AddBasicInfoViewModel.swift */; };
896CE6FE27707E6B00DB474E /* CircleNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */; };
896CE7002770847400DB474E /* CircleFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE6FF2770847400DB474E /* CircleFooterView.swift */; };
896CE7022770892F00DB474E /* CircleGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896CE7012770892F00DB474E /* CircleGridView.swift */; };
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 */; };
89AD5573276DD790001658E0 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AD5572276DD790001658E0 /* SplashView.swift */; };
89AD5575276DE724001658E0 /* PhoneCodeVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AD5574276DE724001658E0 /* PhoneCodeVerificationView.swift */; };
89AD5578276DEAB3001658E0 /* LogoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AD5577276DEAB3001658E0 /* LogoHeaderView.swift */; };
@@ -46,7 +53,6 @@
89BDCDEF2769B50C00577829 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89BDCDEE2769B50C00577829 /* ContactsView.swift */; };
89BDCDF12769E64200577829 /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89BDCDF02769E64200577829 /* Messages.swift */; };
89F05B9B276A87920002E9C7 /* ContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */; };
89F05B9F276A8D360002E9C7 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05B9E276A8D360002E9C7 /* Contact.swift */; };
89F05BA4276ACC060002E9C7 /* ContactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05BA3276ACC060002E9C7 /* ContactService.swift */; };
89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */; };
89F1386D2767EB19006680ED /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F1386C2767EB19006680ED /* SignInView.swift */; };
@@ -74,6 +80,7 @@
/* Begin PBXFileReference section */
8904DEC7277055E90071E6CA /* OnboardingTrioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingTrioView.swift; sourceTree = "<group>"; };
89110713277166D6005797FA /* UserFriends.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriends.swift; sourceTree = "<group>"; };
891A15DD27653A7000B26659 /* nirvana-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nirvana-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
891A15E027653A7000B26659 /* nirvana_iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = nirvana_iosApp.swift; sourceTree = "<group>"; };
891A15E227653A7000B26659 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -97,6 +104,12 @@
894E80BD276ED7A700624B72 /* avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = avatars.swift; sourceTree = "<group>"; };
894E80C0276EFD0400624B72 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
894E80C227702E5800624B72 /* AddBasicInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBasicInfoViewModel.swift; sourceTree = "<group>"; };
896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleNavigationView.swift; sourceTree = "<group>"; };
896CE6FF2770847400DB474E /* CircleFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleFooterView.swift; sourceTree = "<group>"; };
896CE7012770892F00DB474E /* CircleGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleGridView.swift; sourceTree = "<group>"; };
896CE7042770AE5D00DB474E /* InboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxView.swift; sourceTree = "<group>"; };
896CE70627711BAB00DB474E /* FindFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindFriendsView.swift; sourceTree = "<group>"; };
896CE70827712CED00DB474E /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
89AD5572276DD790001658E0 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
89AD5574276DE724001658E0 /* PhoneCodeVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneCodeVerificationView.swift; sourceTree = "<group>"; };
89AD5577276DEAB3001658E0 /* LogoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoHeaderView.swift; sourceTree = "<group>"; };
@@ -113,7 +126,6 @@
89BDCDEE2769B50C00577829 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
89BDCDF02769E64200577829 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = "<group>"; };
89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsViewModel.swift; sourceTree = "<group>"; };
89F05B9E276A8D360002E9C7 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
89F05BA3276ACC060002E9C7 /* ContactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactService.swift; sourceTree = "<group>"; };
89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsPickerView.swift; sourceTree = "<group>"; };
89F1386C2767EB19006680ED /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
@@ -205,6 +217,7 @@
891A15F62765639B00B26659 /* Views */ = {
isa = PBXGroup;
children = (
896CE7032770AE3400DB474E /* Inbox */,
894E80BF276EFCF500624B72 /* AudioPlayer */,
89F18AF3276ECF1800115B1E /* AddBasicInfo */,
89F18AF2276ECD5D00115B1E /* OnBoarding */,
@@ -261,6 +274,7 @@
89BDCDE62769616600577829 /* UIImage+Extension.swift */,
89F18AEB276E92BF00115B1E /* CustomExtensions.swift */,
894E80BD276ED7A700624B72 /* avatars.swift */,
896CE70827712CED00DB474E /* StringExtension.swift */,
);
path = Globals;
sourceTree = "<group>";
@@ -270,7 +284,7 @@
children = (
89BDCDF02769E64200577829 /* Messages.swift */,
891A160D27657CEA00B26659 /* User.swift */,
89F05B9E276A8D360002E9C7 /* Contact.swift */,
89110713277166D6005797FA /* UserFriends.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -301,6 +315,14 @@
path = AudioPlayer;
sourceTree = "<group>";
};
896CE7032770AE3400DB474E /* Inbox */ = {
isa = PBXGroup;
children = (
896CE7042770AE5D00DB474E /* InboxView.swift */,
);
path = Inbox;
sourceTree = "<group>";
};
89AD5571276DD77D001658E0 /* SplashView */ = {
isa = PBXGroup;
children = (
@@ -331,6 +353,9 @@
children = (
89BCABE4276B3D58009E9B10 /* InnerCircleView.swift */,
89BCABE6276B3D64009E9B10 /* InnerCircleViewModel.swift */,
896CE6FD27707E6B00DB474E /* CircleNavigationView.swift */,
896CE6FF2770847400DB474E /* CircleFooterView.swift */,
896CE7012770892F00DB474E /* CircleGridView.swift */,
);
path = InnerCircle;
sourceTree = "<group>";
@@ -359,6 +384,7 @@
89BDCDEE2769B50C00577829 /* ContactsView.swift */,
89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */,
89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */,
896CE70627711BAB00DB474E /* FindFriendsView.swift */,
);
path = Contacts;
sourceTree = "<group>";
@@ -502,9 +528,13 @@
buildActionMask = 2147483647;
files = (
891A15E327653A7000B26659 /* ContentView.swift in Sources */,
896CE7002770847400DB474E /* CircleFooterView.swift in Sources */,
894E80BE276ED7A700624B72 /* avatars.swift in Sources */,
89F05B9F276A8D360002E9C7 /* Contact.swift in Sources */,
896CE7052770AE5D00DB474E /* InboxView.swift in Sources */,
891A160327656FC700B26659 /* HomeView.swift in Sources */,
896CE70927712CED00DB474E /* StringExtension.swift in Sources */,
89110714277166D6005797FA /* UserFriends.swift in Sources */,
896CE70727711BAB00DB474E /* FindFriendsView.swift in Sources */,
891A160B2765732700B26659 /* HomeViewModel.swift in Sources */,
89BCABEB276D2A09009E9B10 /* PhoneVerificationView.swift in Sources */,
892179E62765FECD001E6C60 /* FooterControlsViewModel.swift in Sources */,
@@ -520,6 +550,7 @@
89BDCDE527695DB900577829 /* NirvanaImage.swift in Sources */,
891A16132765A44300B26659 /* ChatsCarouselViewModel.swift in Sources */,
89F05B9B276A87920002E9C7 /* ContactsViewModel.swift in Sources */,
896CE6FE27707E6B00DB474E /* CircleNavigationView.swift in Sources */,
89AD5573276DD790001658E0 /* SplashView.swift in Sources */,
89F18AEC276E92BF00115B1E /* CustomExtensions.swift in Sources */,
89F18AF1276ECAA300115B1E /* RouterView.swift in Sources */,
@@ -528,6 +559,7 @@
89BCABED276D2B05009E9B10 /* WavesGlassBackgroundView.swift in Sources */,
89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */,
8904DEC8277055E90071E6CA /* OnboardingTrioView.swift in Sources */,
896CE7022770892F00DB474E /* CircleGridView.swift in Sources */,
89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */,
89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */,
89AD557C276DF86A001658E0 /* FirestoreService.swift in Sources */,
+28
View File
@@ -0,0 +1,28 @@
//
// StringExtension.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import Foundation
extension String {
func matchingStrings(regex: String) -> [[String]] {
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
result.range(at: $0).location != NSNotFound
? nsString.substring(with: result.range(at: $0))
: ""
}
}
}
var digits: String {
return components(separatedBy: CharacterSet.decimalDigits.inverted)
.joined()
}
}
-17
View File
@@ -1,17 +0,0 @@
//
// Contact.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/15/21.
//
import Foundation
import Contacts
struct Contact: Identifiable, Hashable {
var id = UUID()
var firstName: String
var lastName: String
var phoneNumbers: [String]
var emailAddresses: [String]
}
+34
View File
@@ -0,0 +1,34 @@
//
// UserFriends.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import Foundation
import Firebase
import FirebaseFirestoreSwift
enum UserFriendsState {
case active
case inactive
}
struct UserFriends: Identifiable, Codable {
@DocumentID var id: String? = UUID().uuidString
var userId: String
var friendId: String
var isActive: Bool
@ServerTimestamp var lastUpdatedTimestamp: Date?
@ServerTimestamp var createdTimestamp: Date?
enum CodingKeys: String, CodingKey {
case id
case userId
case friendId
case isActive
case createdTimestamp
case lastUpdatedTimestamp
}
}
@@ -7,13 +7,15 @@
import Foundation
import Firebase
import FirebaseCore
import FirebaseFirestore
import FirebaseFirestoreSwift
class FirestoreService {
enum Collection: String {
case users = "users"
case messages = "messages"
case user_friends = "user_friends" // associating a user to
case userFriends = "user_friends" // associating a user to
}
private var db = Firestore.firestore()
@@ -50,6 +52,26 @@ class FirestoreService {
}
}
}
func getUserByPhoneNumber(phoneNumber: String, completion: @escaping((_ user: User?) -> ())) {
// TODO: validation here as well to verify that phone number string is even valid
db.collection(Collection.users.rawValue).whereField("phoneNumber", isEqualTo: phoneNumber)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion(nil)
} else {
if !(querySnapshot?.isEmpty)! {
for document in querySnapshot!.documents {
let returnedUser = try? document.data(as: User.self)
completion(returnedUser)
}
} else {
completion(nil)
}
}
}
}
func createUser(user: User) {
do {
@@ -81,4 +103,21 @@ class FirestoreService {
return FieldValue.serverTimestamp()
}
func createOrUpdateUserFriends(userFriend: UserFriends, completion: @escaping((_ state: ServiceState) -> ())) {
do {
if userFriend.id != nil {
let _ = try db.collection(Collection.userFriends.rawValue).addDocument(from: userFriend)
completion(ServiceState.success("created/updated userfriend in firestore service"))
} else {
completion(ServiceState.error(ServiceError(description: "No userfriend id given to firestore service")))
}
} catch {
print("error in creating user friend \(error.localizedDescription)")
completion(ServiceState.error(ServiceError(description: error.localizedDescription)))
}
}
}
+15 -7
View File
@@ -121,26 +121,34 @@ final class AuthSessionStore: ObservableObject, SessionStore {
func setupAuthListen() {
// monitor authentication changes using firebase
self.handler = Auth.auth().addStateDidChangeListener { [weak self] res, user in
self.handler = Auth.auth().addStateDidChangeListener { [weak self] res, authUser in
print("auth listener activated")
guard let self = self else { return }
self.sessionState = user == nil ? SessionState.isLoggedOut : SessionState.isAuthenticated
self.sessionState = authUser == nil ? SessionState.isLoggedOut : SessionState.isAuthenticated
// get the user from the auth table in firebase auth
if let uid = user?.uid {
if let uid = authUser?.uid {
// if we have a user, create a new user model
print("auth listener: Got user: \(uid)")
print("auth listener: phone number: \(user?.phoneNumber)")
print("auth listener: phone number: \(authUser?.phoneNumber)")
// when we have a user get signed in, we can activate the listener for fetching the user's data to keep our auth session store always up to date for all of the ui
self.firestoreService.getUserRealtime(userId: uid) {[weak self] realtimeUpdatedUser in
if realtimeUpdatedUser != nil {
print("up to date user: \(realtimeUpdatedUser)")
self?.user = realtimeUpdatedUser
DispatchQueue.main.async {
if realtimeUpdatedUser != nil {
print("up to date user: \(realtimeUpdatedUser)")
self?.user = realtimeUpdatedUser
}
}
}
// TODO: listener for getting user's sent and received messages
// TODO: listener for getting user's circle members
} else {
// if we don't have a user, set our session to nil
self.user = nil
@@ -102,7 +102,7 @@ struct AddBasicInfoView: View {
Spacer()
// button to save information
Button {
print("attempting to save information")
@@ -1,78 +1,82 @@
////
//// ContactsPickerView.swift
//// nirvana-ios
////
//// Created by Arjun Patel on 12/15/21.
////
//
// ContactsPickerView.swift
// nirvana-ios
//import SwiftUI
//import Contacts
//
// Created by Arjun Patel on 12/15/21.
//struct ContactsPickerView: View {
// @Environment(\.dismiss) var dismiss
//
import SwiftUI
import Contacts
struct ContactPickerView: View {
@Binding var showPicker: Bool
// updates the parent view for it to do something with the new contact
@Binding var selectedContact: CNContact?
var contacts: [String: [CNContact]] = loadContactsGrouped()
var body: some View {
let keys = (Array(contacts.keys) as [String]).sorted()
NavigationView {
List {
ForEach(keys.sorted(), id: \.self) { key in
Section(header: Text(key)) {
ForEach(contacts[key] ?? [CNContact](), id: \.identifier) { contact in
ContactRow(contact: contact, showPicker: self.$showPicker, selectedContact: self.$selectedContact)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationTitle("All Contacts".localized)
.navigationBarItems(trailing: Button(action: {self.showPicker = false}, label: {
Text("Cancel".localized)
}))
}
}
static func loadContactsGrouped() -> [String: [CNContact]] {
let provider = ContactsViewModel()
let contacts = provider.fetchContacts()
let group = Dictionary(grouping: contacts) { (contact) -> String in
return String(contact.familyName.first ?? "?")
}
return group
}
}
struct ContactRow: View {
var contact: CNContact
@Binding var showPicker: Bool
@Binding var selectedContact: CNContact?
var body: some View {
Button(action: {
selectContact()
}) {
HStack {
Text("\(contact.familyName)").fontWeight(.bold)
Text("\(contact.givenName) \(contact.middleName)")
}
.foregroundColor(.black)
}
}
func selectContact() {
self.selectedContact = self.contact
self.showPicker = false
}
}
extension String {
var localized: String {
return NSLocalizedString(self, comment: "")
}
}
// // updates the parent view for it to do something with the new contact
// @State var selectedContact: CNContact?
//
// var contacts: [String: [CNContact]] = loadContactsGrouped()
//
// var body: some View {
// let keys = (Array(contacts.keys) as [String]).sorted()
// NavigationView {
// List {
// ForEach(keys.sorted(), id: \.self) { key in
// Section(header: Text(key)) {
// ForEach(contacts[key] ?? [CNContact](), id: \.identifier) { contact in
// ContactRow(contact: contact, selectedContact: self.$selectedContact)
// }
// }
// }
// }
// .listStyle(GroupedListStyle())
// .navigationTitle("All Contacts".localized)
// .navigationBarItems(trailing: Button(action: {
// dismiss()
//
// }, label: {
// Text("Cancel".localized)
// }))
// }
// }
//
// static func loadContactsGrouped() -> [String: [CNContact]] {
// let provider = ContactsViewModel()
// let contacts = provider.fetchContacts()
// print(contacts)
// let group = Dictionary(grouping: contacts) { (contact) -> String in
// return String(contact.familyName.first ?? "?")
// }
//
// print(group)
//
// return group
// }
//}
//
//
//struct ContactRow: View {
// var contact: CNContact
// @Binding var selectedContact: CNContact?
//
// var body: some View {
// Button(action: {
// selectContact()
// }) {
// HStack {
// Text("\(contact.familyName)").fontWeight(.bold)
// Text("\(contact.givenName) \(contact.middleName)")
// }
// .foregroundColor(.black)
// }
// }
//
// func selectContact() {
// self.selectedContact = self.contact
// }
//}
//
//extension String {
// var localized: String {
// return NSLocalizedString(self, comment: "")
// }
//}
@@ -42,7 +42,7 @@ struct ContactsView: View {
.shadow(radius:10)
})
Text(selectedContact != nil ? "Selected: \((selectedContact?.familyName)!) \((selectedContact?.givenName)!)" : "Nothing selected".localized)
Text(selectedContact != nil ? "Selected: \((selectedContact?.familyName)!) \((selectedContact?.givenName)!)" : "Nothing selected")
Button {
self.navigationStack.push(InnerCircleView())
@@ -53,7 +53,7 @@ struct ContactsView: View {
Spacer()
}
.sheet(isPresented: self.$showPicker) {
ContactPickerView(showPicker: self.$showPicker, selectedContact: self.$selectedContact)
// ContactPickerView(showPicker: self.$showPicker, selectedContact: self.$selectedContact)
}
.padding()
.background(NirvanaColor.bgLightGrey)
@@ -11,17 +11,18 @@ import SwiftUI
class ContactsViewModel : ObservableObject {
@Published var showPermissionAlert = false
@Published var contacts: [String: ContactsViewModelContact] = [:]
let store: CNContactStore
let keys: [CNKeyDescriptor]
private var firestoreService = FirestoreService()
let store: CNContactStore = CNContactStore()
let keys: [CNKeyDescriptor] = [CNContactImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
init() {
store = CNContactStore()
keys = [CNContactImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
self.fetchContacts()
}
func openSettings() {
@@ -30,37 +31,86 @@ class ContactsViewModel : ObservableObject {
}
func requestAccess() {
store.requestAccess(for: .contacts) {
(granted, error) in
store.requestAccess(for: .contacts) {(granted, error) in
// TODO: do something useful with completionHandler
return
}
// TODO do something useful with completionHandler
}
func fetchContacts(sortOrder: CNContactSortOrder = .userDefault) -> [CNContact] {
func fetchContacts() {
let authStatus = CNContactStore.authorizationStatus(for: .contacts)
if authStatus == .notDetermined {
requestAccess()
} else if authStatus == .restricted {
// TODO prompt user that app is useless without access to contacts
// TODO: prompt user that app is useless without access to contacts
self.showPermissionAlert = true
}
let fetchRequest = CNContactFetchRequest(keysToFetch: keys)
fetchRequest.sortOrder = sortOrder
var fetchedContacts = [CNContact]()
fetchRequest.sortOrder = .userDefault
do {
try store.enumerateContacts(with: fetchRequest) {
(contact, stop) in
fetchedContacts.append(contact)
try store.enumerateContacts(with: fetchRequest) {(contact, stop) in
// only checking if american number or not for now
var cnPhoneNumber = contact.phoneNumbers.first?.value.stringValue
let contactDisplayName = contact.givenName + " " + contact.familyName
// strip out nondigits
// add or keep country code
// TODO: take out hard coding to US country code
var formattedNumber = cnPhoneNumber?.digits.suffix(10)
// only show contacts if they have a phone number
if formattedNumber != nil && formattedNumber?.count ?? 0 > 9 {
formattedNumber = "+1" + formattedNumber! // adding country code
let contactNumber = String(formattedNumber!)
var contactVm = ContactsViewModelContact(cnName: contactDisplayName, cnPhoneNumber: contactNumber, sortingProp: contactDisplayName)
// check if the contact is an existing user by phone number
self.firestoreService.getUserByPhoneNumber(phoneNumber: contactNumber) {[weak self] returnedUser in
DispatchQueue.main.async {
if returnedUser == nil {
// do nothing, keep contactsVm object user property nil
contactVm.isExisting = false
contactVm.sortingProp = contactVm.cnName
} else {
// adding user details if this contact is existing user
contactVm.sortingProp = "!" // adding for sorting purposes
contactVm.user = returnedUser
contactVm.isExisting = true
print("existing user: \(contactVm.cnName)")
}
self?.contacts[contactVm.sortingProp] = contactVm
}
}
}
}
} catch {
print("Unable to fetch contacts. \(error)")
}
}
func addOrActivateFriendToCircle(userId: String, friendId: String) {
// setting timestamps to nil to make sure that new server timestamp is set
var userFriend = UserFriends(userId: userId, friendId: friendId, isActive: true, lastUpdatedTimestamp: nil, createdTimestamp: nil)
return fetchedContacts
self.firestoreService.createOrUpdateUserFriends(userFriend: userFriend) {[weak self] res in
print(res)
}
}
}
struct ContactsViewModelContact : Identifiable {
var id : String = UUID().uuidString
var user: User?
var cnName: String
var cnPhoneNumber: String
var isExisting: Bool = false
var sortingProp: String
}
@@ -0,0 +1,148 @@
//
// FindFriendsView.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import SwiftUI
import Contacts
import NavigationStack
struct FindFriendsView: View {
@Environment(\.dismiss) var dismiss
@StateObject var contactsVM = ContactsViewModel()
@State private var searchQuery: String = ""
var body: some View {
NavigationView {
// TODO: add search bar + have secondary sort
List {
ForEach(searchItems, id: \.self) { key in
if let currContact = self.contactsVM.contacts[key] {
ListContactRow(contactsVM: self.contactsVM, contact: currContact)
}
}
}
.searchable(text: self.$searchQuery)
.navigationTitle("Find Friends")
.navigationBarItems(trailing: Button(action: {
dismiss()
}, label: {
Text("Cancel")
}))
}
}
var searchItems: [String] {
let keys = (Array(self.contactsVM.contacts.keys) as [String]).sorted()
if self.searchQuery.isEmpty {
return keys
} else {
return keys.filter { $0.contains(self.searchQuery) }
}
}
}
struct FindFriendsView_Previews: PreviewProvider {
static var previews: some View {
FindFriendsView().environmentObject(AuthSessionStore())
}
}
struct ListContactRow: View {
@EnvironmentObject var navigationStack : NavigationStack
@EnvironmentObject var authSessionStore: AuthSessionStore
var contactsVM: ContactsViewModel
var contact:ContactsViewModelContact
@State var showAlert = false
@State var alertText = "🌱Confirm Arjun into Your Circle?"
@State var alertMessage = "Note: You can have a maximium of 12 people in your circle and you have 5 swaps left!"
var body: some View {
if contact.isExisting { // add to circle
// TODO: check if this contact is already in user's circle
HStack(alignment: .center, spacing: 0) {
Image(contact.user?.avatar ?? Avatars.avatarSystemNames[1])
.resizable()
.scaledToFit()
.clipShape(Circle())
.frame(width: 40, height: 40)
.padding(.trailing, 10)
Text(contact.cnName)
.font(.headline)
Spacer()
Button {
print("showing alert now")
// TODO: show alert if circle is full
self.showAlert.toggle()
} label: {
Label("Add", systemImage: "plus.circle")
.font(.title2)
.foregroundColor(NirvanaColor.solidTeal)
}
}
.alert(self.alertText, isPresented: self.$showAlert) {
Button("Add \(contact.cnName)") {
print("adding contact to circle")
// call method in vm to get it done, then navigate to the circle
self.contactsVM.addOrActivateFriendToCircle(userId: self.authSessionStore.user!.id!, friendId: (contact.user!.id)!)
self.navigationStack.push(InnerCircleView())
}
} message: {
Text(self.alertMessage)
}
}
else { // invite button to text the person
HStack(alignment: .center, spacing: 0) {
Image(Avatars.avatarSystemNames[1])
.resizable()
.scaledToFit()
.clipShape(Circle())
.frame(width: 40, height: 40)
.blur(radius: 8)
.padding(.trailing, 10)
Text(contact.cnName)
.font(.headline)
Spacer()
Button {
print("inviting user now")
// text message to him/her
if contact.cnPhoneNumber != nil {
let sms: String = "sms://\(contact.cnPhoneNumber)&body=Join me on Nirvana! I sent you a message there! https://usenirvana.com"
let strUrl: String = sms.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
UIApplication.shared.open(URL(string: strUrl)!, options: [:], completionHandler: nil)
} else {
print("phone number is nil of contact")
}
} label: {
Label("Invite", systemImage: "paperplane")
.font(.title2)
.foregroundColor(Color.orange)
}
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
//
// InboxView.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import SwiftUI
struct InboxView: View {
var body: some View {
Text("This is the inbox where you can see a bunch of inboxes")
//TODO: add illustration if there is no one in inbox, but have button to open sheet for contacts with the sheet view object
}
}
struct InboxView_Previews: PreviewProvider {
static var previews: some View {
InboxView()
}
}
@@ -0,0 +1,73 @@
//
// CircleFooterView.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import SwiftUI
import NavigationStack
struct CircleFooterView: View {
@EnvironmentObject var navigationStack: NavigationStack
@EnvironmentObject var authSessionStore: AuthSessionStore
var body: some View {
// TODO: do cool animations with this footer background fill in while recording or playing a message
ZStack(alignment:.bottomTrailing) {
VStack(alignment:.center) {
Spacer()
HStack {
Image("Artboards_Diversity_Avatars_by_Netguru-1")
.resizable()
.scaledToFit()
.background(NirvanaColor.teal.opacity(0.1))
.frame(width: 40, height: 40)
.clipShape(Circle())
.padding(5)
VStack (alignment: .leading) {
Text("Sarth Shah")
.font(.footnote)
.foregroundColor(NirvanaColor.light)
Text("sent 20 minutes ago")
.font(.caption)
.foregroundColor(.gray)
}
Spacer()
Label("Replay", systemImage: "repeat.1.circle.fill")
.font(.title2)
.labelStyle(.iconOnly)
.foregroundColor(Color.orange)
Spacer(minLength: 65)
}
.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)
}
FooterControlsView()
}
.padding()
// .offset(
// x:0,
// y:self.selectedUserIndex == nil ? 150 : 0
// )
// .animation(Animation.spring(), value: self.selectedUserIndex)
}
}
struct CircleFooterView_Previews: PreviewProvider {
static var previews: some View {
CircleFooterView()
}
}
@@ -0,0 +1,192 @@
//
// CircleGridView.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import SwiftUI
import NavigationStack
struct CircleGridView: View {
@EnvironmentObject var authSessionStore: AuthSessionStore
@EnvironmentObject var navigationStack: NavigationStack
let universalSize = UIScreen.main.bounds
// magic variables for grid
// TODO: make the top left person or one horizontal person be in the center of screen...makes the top honeycomb pop
private static var numberOfItems: Int = 12
private static let size: CGFloat = UIScreen.main.bounds.height*0.15 // scaling with screen size
private static let spacingBetweenColumns: CGFloat = 0
private static let spacingBetweenRows: CGFloat = 0
private static let totalColumns: Int = Int(log2(Double(Self.numberOfItems))) // scaling the circles and calculating column count
@State private var selectedUserIndex: Int? = nil
// TODO: change the size to adaptive or something to make outer ring items shrink their overall size and fit better
let gridItems: [GridItem] = Array(
repeating: GridItem(.fixed(Self.size), spacing: spacingBetweenColumns, alignment: .center),
count: totalColumns)
private let big:CGFloat = 1
private let medium:CGFloat = 0.75
private let small:CGFloat = 0.5
private let supersmall:CGFloat = 0.3
// TODO: centered honeycomb + side: maybe have two honeycombs: one in the center of 7 people and then everyone else surrounding
// TODO: maybe make our "center" offset to a little to the top left to make it more honeycomb from top left
private let center: CGPoint = CGPoint(x: UIScreen.main.bounds.width*0.5,y: UIScreen.main.bounds.height*0.5)
var body: some View {
gridContent
}
private var gridContent: some View {
// main communication hub
// TODO: client side, sort the honeycomb from top left to bottom right
// based on most close friends to least
ScrollViewReader {scrollReaderValue in
ScrollView([.horizontal, .vertical], showsIndicators: false) {
LazyVGrid(
columns: gridItems,
alignment: .center,
spacing: Self.spacingBetweenRows
) {
ForEach(0..<Self.numberOfItems) { value in
GeometryReader {gridProxy in
let scale = getScale(proxy: gridProxy, itemNumber: value)
ZStack(alignment: .topTrailing) {
Circle()
.foregroundColor(self.getBubbleTint(userIndex: value)) // different color for a selected user
.blur(radius: 8)
.cornerRadius(100)
Image("Artboards_Diversity_Avatars_by_Netguru-\(value + 1)")
.resizable()
.scaledToFit()
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 20)
}
.scaleEffect(scale)
.padding(scale * 5)
} // geometry reader
.offset(
x: honeycombOffSetX(value),
y: 0
)
.id(value) // id for scrollviewreader
.frame(height: Self.size)
.onTapGesture {
withAnimation {
// if user had previously selected user, put nil as a toggle
if self.selectedUserIndex == value {
self.selectedUserIndex = nil
} else {
self.selectedUserIndex = value
}
}
print("the selected item is: \(value)")
}
.onLongPressGesture { // haptics for recording
let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
impactHeavy.impactOccurred()
}
// .animation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true), value: self.usersWithNewMessage)
.animation(Animation.spring())
}
} // TODO: add padding based on if we are on any cornering item to allow the bubble to enlargen
.padding(.trailing, Self.size / 2 + Self.spacingBetweenColumns / 2) // because of the offset of last column
.padding(.top, Self.size / 2 + Self.spacingBetweenRows / 2) // because we are going under the nav bar
}// scrollview
.onAppear {
//TODO: was causing problems so commenting out
//may not need anymore with bottom nav activation
// scrollReaderValue.scrollTo(Self.numberOfItems / 2)
}
} // scrollview reader
}
private func getBubbleTint(userIndex: Int) -> Color {
if (userIndex == self.selectedUserIndex) { // user clicked on this user
return NirvanaColor.dimTeal.opacity(0.4)
}
// else if self.usersWithNewMessage.contains(userIndex) { // this user has a message
// return Color.orange.opacity(0.8)
// }
return Color.white.opacity(0.4)
}
// getting the proxy of an individual item
// and decoding into a scale that the item should take
private func getScale(proxy: GeometryProxy, itemNumber: Int) -> CGFloat {
// if this user is selected
if itemNumber == self.selectedUserIndex {
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
// if self.usersWithNewMessage.contains(itemNumber) {
// return big + 0.2
// }
let posRelToGrid = proxy.frame(in: .global) // relative to the entire screen
let midX = getRowNumber(itemNumber) % 2 == 0 ? posRelToGrid.midX + (Self.size / 2) + (Self.spacingBetweenColumns / 2) : posRelToGrid.midX
let midY = posRelToGrid.midY
let xdelta = abs(midX - self.center.x)
let ydelta = abs(midY - self.center.y)
let innerCircleXAcceptance = Self.size * 0.75
let innerCircleYAcceptance = Self.size * 0.75
let outerRingXAcceptance = self.universalSize.width * 0.4
let outerRingYAcceptance = self.universalSize.height * 0.25
let wayOutRingXAcceptance = self.universalSize.width * 0.425
let wayOutRingYAcceptance = self.universalSize.height * 0.375
if xdelta <= innerCircleXAcceptance && ydelta < innerCircleYAcceptance {
return big
} else if xdelta <= outerRingXAcceptance && ydelta < outerRingYAcceptance {
return medium
} else if xdelta <= wayOutRingXAcceptance && ydelta < wayOutRingYAcceptance {
return small
}
return supersmall
}
private static func distanceBetweenPoints(p1: CGPoint, p2: CGPoint) -> CGFloat {
let xDist = abs(p2.x - p1.x)
let yDist = abs(p2.y - p2.y)
return CGFloat(
sqrt(
pow(xDist, 2) + pow(yDist, 2)
)
)
}
//get row number given a value/item number in the list
private func getRowNumber(_ value: Int) -> Int {
return value / Self.totalColumns
}
private func honeycombOffSetX(_ value: Int) -> CGFloat {
let rowNumber = self.getRowNumber(value)
// make every even row have the honeycomb offset
if rowNumber % 2 == 0 {
return Self.size / 2 + Self.spacingBetweenColumns / 2 // account for the column spacing from before
}
return 0
}
}
struct CircleGridView_Previews: PreviewProvider {
static var previews: some View {
CircleGridView()
}
}
@@ -0,0 +1,150 @@
//
// NavigationView.swift
// nirvana-ios
//
// Created by Arjun Patel on 12/20/21.
//
import SwiftUI
import NavigationStack
struct CircleNavigationView: View {
@EnvironmentObject var navigationStack: NavigationStack
@EnvironmentObject var authSessionStore: AuthSessionStore
@Binding var sheetView: SheetView?
var body: some View {
HStack(alignment: .center) {
Menu {
Button(role: .destructive) {
print("manage inbox")
self.sheetView = .inbox
} label: {
Label("inbox", systemImage: "envelope.badge")
.foregroundColor(NirvanaColor.teal)
}
Button {
print("going to show contacts now")
self.sheetView = .contacts
} label: {
Label("add peeps", systemImage: "person.2.wave.2")
.foregroundColor(NirvanaColor.teal)
}
Button {
self.navigationStack.push(AddBasicInfoView())
} label: {
Label("profile", systemImage: "person.crop.circle")
.foregroundColor(NirvanaColor.teal)
}
Button {
self.navigationStack.push(OnboardingTrioView())
} label: {
Label("why?", systemImage: "leaf")
.foregroundColor(NirvanaColor.teal)
}
Button {
print("navigate to usenirvana.com")
if let url = URL(string: "https://usenirvana.com") {
UIApplication.shared.open(url)
}
} label: {
Label("about us", systemImage: "globe.americas")
.foregroundColor(NirvanaColor.teal)
}
Button(role: .destructive) {
print("log out button clicked")
self.authSessionStore.logOut()
// once logged out, then the listener will listen and router view will take care of us thereafter
} label: {
Label("log out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(NirvanaColor.teal)
}
} label: {
if self.authSessionStore.user?.avatar == nil {
Image("Artboards_Diversity_Avatars_by_Netguru-1")
.resizable()
.scaledToFit()
.background(NirvanaColor.teal.opacity(0.1))
.blur(radius: 5)
.frame(width: 40, height: 40)
.clipShape(Circle())
.padding(5)
} else {
Image((self.authSessionStore.user?.avatar)!)
.resizable()
.scaledToFit()
.background(NirvanaColor.teal.opacity(0.5))
.frame(width: 40, height: 40)
.clipShape(Circle())
.padding(5)
}
}
ScrollView(.horizontal) {
HStack {
Button {
} label: {
Label("circle", systemImage: "peacesign")
.font(.caption2)
.foregroundColor(Color.white)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color.teal.opacity(0.5))
).overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(01), lineWidth: 1)
)
}
Button {
} label: {
Label("groups", systemImage: "rectangle.3.group")
.font(.caption2)
.foregroundColor(Color.gray)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(1), lineWidth: 1)
)
}
Button {
} label: {
Label("work", systemImage: "suitcase")
.font(.caption2)
.foregroundColor(Color.gray)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(1), lineWidth: 1)
)
}
}
}
}
.padding(.horizontal)
}
}
struct CircleNavigationView_Previews: PreviewProvider {
static var previews: some View {
CircleNavigationView(sheetView: .constant(SheetView.contacts)).environmentObject(AuthSessionStore(isPreview: true))
}
}
@@ -8,15 +8,19 @@
import SwiftUI
import NavigationStack
enum SheetView : Identifiable {
var id: Self { self }
case contacts
case inbox
}
struct InnerCircleView: View {
@EnvironmentObject var authSessionStore: AuthSessionStore
@EnvironmentObject var navigationStack: NavigationStack
let universalSize = UIScreen.main.bounds
@State var sheetView: SheetView? = nil
// TESTING
// users who will have a thumping thing because they sent a message
@State var usersWithNewMessage: [Int] = []
let universalSize = UIScreen.main.bounds
var body: some View {
ZStack {
@@ -24,356 +28,31 @@ struct InnerCircleView: View {
WavesGlassBackgroundView()
// content
gridContent
CircleGridView()
// header
VStack(alignment: .leading) {
header
bubbleNavigation
CircleNavigationView(sheetView: self.$sheetView)
Spacer()
}
// Path { path in
//
// //draw the axes
// path.move(to: CGPoint(x:0, y:universalSize.height*0.5 ))
//
// path.addLine(to: CGPoint(x:universalSize.width, y: universalSize.height*0.5))
//
// path.move(to: CGPoint(x:universalSize.width*0.5, y:0 ))
//
// path.addLine(to: CGPoint(x:universalSize.width * 0.5, y: universalSize.height))
//
//
// //TODO: draw the acceptance boxes
// }
// .stroke(Color.black)
// .edgesIgnoringSafeArea(.all)
footerNavigation
CircleFooterView()
}
.onAppear() {
// TESTING
// fake like these users sent a message to view
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.usersWithNewMessage = [1, 5, 10]
}
}
}
// TODO: do cool animations with this footer background fill in while
// recording or playing a message
var footerNavigation: some View {
ZStack(alignment:.bottomTrailing) {
VStack(alignment:.center) {
Spacer()
HStack {
if self.selectedUserIndex != nil {
Image("Artboards_Diversity_Avatars_by_Netguru-\(self.selectedUserIndex! + 1)")
.resizable()
.scaledToFit()
.background(NirvanaColor.teal.opacity(0.1))
.frame(width: 40, height: 40)
.clipShape(Circle())
.padding(5)
}
VStack (alignment: .leading) {
Text("Sarth Shah")
.font(.footnote)
.foregroundColor(NirvanaColor.light)
Text("sent 20 minutes ago")
.font(.caption)
.foregroundColor(.gray)
}
Spacer()
Label("Replay", systemImage: "repeat.1.circle.fill")
.font(.title2)
.labelStyle(.iconOnly)
.foregroundColor(Color.orange)
Spacer(minLength: 65)
}
.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)
}
FooterControlsView()
}
.padding()
.offset(
x:0,
y:self.selectedUserIndex == nil ? 150 : 0
)
.animation(Animation.spring(), value: self.selectedUserIndex)
}
var bubbleNavigation : some View {
HStack(alignment: .center) {
Button {
} label: {
Label("circle", systemImage: "peacesign")
.font(.caption2)
.foregroundColor(Color.white)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color.teal.opacity(0.5))
).overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(01), lineWidth: 1)
)
}
Button {
} label: {
Label("groups", systemImage: "rectangle.3.group")
.font(.caption2)
.foregroundColor(Color.gray)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(1), lineWidth: 1)
)
}
Button {
} label: {
Label("work", systemImage: "suitcase")
.font(.caption2)
.foregroundColor(Color.gray)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.strokeBorder(Color.white.opacity(1), lineWidth: 1)
)
}
}
.padding(.horizontal)
}
// magic variables for grid
// TODO: change the number of columns based on the number of items
// TODO: make the top left person or one horizontal person be in the center of screen...makes the top honeycomb pop
private static var numberOfItems: Int = 12
private static let size: CGFloat = UIScreen.main.bounds.height*0.15 // scaling with screen size
private static let spacingBetweenColumns: CGFloat = 0
private static let spacingBetweenRows: CGFloat = 0
private static let totalColumns: Int = Int(log2(Double(Self.numberOfItems))) // scaling the circles and calculating column count
@State private var selectedUserIndex: Int? = nil
// TODO: change the size to adaptive or something to make outer ring items shrink their overall size and fit better
let gridItems: [GridItem] = Array(
repeating: GridItem(.fixed(Self.size), spacing: spacingBetweenColumns, alignment: .center),
count: totalColumns)
private let big:CGFloat = 1
private let medium:CGFloat = 0.75
private let small:CGFloat = 0.5
private let supersmall:CGFloat = 0.3
// TODO: centered honeycomb + side: maybe have two honeycombs: one in the center of 7 people and then everyone else surrounding
// TODO: maybe make our "center" offset to a little to the top left to make it more honeycomb from top left
private let center: CGPoint = CGPoint(x: UIScreen.main.bounds.width*0.5,y: UIScreen.main.bounds.height*0.5)
private var gridContent: some View {
// main communication hub
// TODO: client side, sort the honeycomb from top left to bottom right
// based on most close friends to least
ScrollViewReader {scrollReaderValue in
ScrollView([.horizontal, .vertical], showsIndicators: false) {
LazyVGrid(
columns: gridItems,
alignment: .center,
spacing: Self.spacingBetweenRows
) {
ForEach(0..<Self.numberOfItems) { value in
GeometryReader {gridProxy in
let scale = getScale(proxy: gridProxy, itemNumber: value)
ZStack(alignment: .topTrailing) {
Circle()
.foregroundColor(self.getBubbleTint(userIndex: value)) // different color for a selected user
.blur(radius: 8)
.cornerRadius(100)
Image("Artboards_Diversity_Avatars_by_Netguru-\(value + 1)")
.resizable()
.scaledToFit()
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 20)
}
.scaleEffect(scale)
.padding(scale * 5)
} // geometry reader
.offset(
x: honeycombOffSetX(value),
y: 0
)
.id(value) // id for scrollviewreader
.frame(height: Self.size)
.onTapGesture {
withAnimation {
// if user had previously selected user, put nil as a toggle
if self.selectedUserIndex == value {
self.selectedUserIndex = nil
} else {
self.selectedUserIndex = value
}
}
print("the selected item is: \(value)")
}
.animation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true), value: self.usersWithNewMessage)
.animation(Animation.spring())
}
} // lazyvgrid
.padding(.trailing, Self.size / 2 + Self.spacingBetweenColumns / 2) // because of the offset of last column
.padding(.top, Self.size / 2 + Self.spacingBetweenRows / 2) // because we are going under the nav bar
}// scrollview
.onAppear {
//TODO: was causing problems so commenting out
//may not need anymore with bottom nav activation
// scrollReaderValue.scrollTo(Self.numberOfItems / 2)
}
} // scrollview reader
}
private func getBubbleTint(userIndex: Int) -> Color {
if (userIndex == self.selectedUserIndex) { // user clicked on this user
return NirvanaColor.dimTeal.opacity(0.4)
} else if self.usersWithNewMessage.contains(userIndex) { // this user has a message
return Color.orange.opacity(0.8)
}
return Color.white.opacity(0.4)
}
// getting the proxy of an individual item
// and decoding into a scale that the item should take
private func getScale(proxy: GeometryProxy, itemNumber: Int) -> CGFloat {
// if this user is selected
if itemNumber == self.selectedUserIndex {
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
if self.usersWithNewMessage.contains(itemNumber) {
return big + 0.2
}
let posRelToGrid = proxy.frame(in: .global) // relative to the entire screen
let midX = getRowNumber(itemNumber) % 2 == 0 ? posRelToGrid.midX + (Self.size / 2) + (Self.spacingBetweenColumns / 2) : posRelToGrid.midX
let midY = posRelToGrid.midY
let xdelta = abs(midX - self.center.x)
let ydelta = abs(midY - self.center.y)
let innerCircleXAcceptance = Self.size * 0.75
let innerCircleYAcceptance = Self.size * 0.75
let outerRingXAcceptance = self.universalSize.width * 0.4
let outerRingYAcceptance = self.universalSize.height * 0.25
let wayOutRingXAcceptance = self.universalSize.width * 0.425
let wayOutRingYAcceptance = self.universalSize.height * 0.375
if xdelta <= innerCircleXAcceptance && ydelta < innerCircleYAcceptance {
return big
} else if xdelta <= outerRingXAcceptance && ydelta < outerRingYAcceptance {
return medium
} else if xdelta <= wayOutRingXAcceptance && ydelta < wayOutRingYAcceptance {
return small
}
return supersmall
}
private static func distanceBetweenPoints(p1: CGPoint, p2: CGPoint) -> CGFloat {
let xDist = abs(p2.x - p1.x)
let yDist = abs(p2.y - p2.y)
return CGFloat(
sqrt(
pow(xDist, 2) + pow(yDist, 2)
)
)
}
//get row number given a value/item number in the list
private func getRowNumber(_ value: Int) -> Int {
return value / Self.totalColumns
}
private func honeycombOffSetX(_ value: Int) -> CGFloat {
let rowNumber = self.getRowNumber(value)
// make every even row have the honeycomb offset
if rowNumber % 2 == 0 {
return Self.size / 2 + Self.spacingBetweenColumns / 2 // account for the column spacing from before
}
return 0
}
private var header: some View {
HStack(alignment: .center) {
Image("undraw_handcrafts_leaf")
.resizable()
.frame(width: 20.0, height: 32.132)
.padding(.leading, 20)
Spacer()
Image(systemName: "message.and.waveform")
.foregroundColor(Color.red.opacity(1))
.font(.title2)
Menu {
Button("Edit Profile") {
self.navigationStack.push(AddBasicInfoView())
}
Button("Friends") {
print("manage friends page")
}
Button("Log out") {
print("log out button clicked")
self.authSessionStore.logOut()
// once logged out, then the listener will listen and router view will take care of us thereafter
}
} label: {
Image("Artboards_Diversity_Avatars_by_Netguru-51")
.resizable()
.scaledToFit()
.background(NirvanaColor.teal.opacity(0.1))
.frame(width: 40, height: 40)
.clipShape(Circle())
.padding(5)
.sheet(item: self.$sheetView) {page in
switch page {
case SheetView.contacts:
FindFriendsView()
case SheetView.inbox:
InboxView()// test one for now
default:
Text("asdf")
}
}
.frame(maxWidth: .infinity)
.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(.horizontal)
.onAppear() {
}
}
}