diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
index 21fe18470..30356f083 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Profile/RecommendationGroupView.swift
@@ -12,16 +12,58 @@ import Views
self.recommendationGroup = recommendationGroup
}
- func loadGroups(dataService _: DataService) async {
- isLoading = true
+ var nonAdmins: [InternalUserProfile] {
+ recommendationGroup.members.filter { member in
+ !recommendationGroup.admins.contains(where: { member.id == $0.id })
+ }
+ }
+}
-// do {
-// recommendationGroups = try await dataService.recommendationGroups()
-// } catch {
-// networkError = true
-// }
+private struct SmallUserCard: View {
+ let data: ProfileCardData
+ var size: CGFloat
- isLoading = false
+ public init(data: ProfileCardData, size: CGFloat = 36) {
+ self.data = data
+ self.size = size
+ }
+
+ public var body: some View {
+ HStack(alignment: .center, spacing: 16) {
+ Group {
+ AsyncImage(
+ url: data.imageURL,
+ content: { $0.resizable() },
+ placeholder: {
+ Image(systemName: "person.crop.circle")
+ .resizable()
+ .foregroundColor(.appGrayTextContrast)
+ }
+ )
+ }
+ .padding(.leading, 0)
+ .aspectRatio(contentMode: .fill)
+ .frame(width: size, height: size, alignment: .center)
+ .clipShape(Circle())
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(data.name)
+ .font(.appBody)
+ .foregroundColor(.appGrayTextContrast)
+ .lineLimit(1)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Text("@\(data.username)")
+ .font(.appCaption)
+ .foregroundColor(.appGrayText)
+ .lineLimit(1)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .frame(maxWidth: .infinity)
+ .multilineTextAlignment(.leading)
+ .padding(0)
+ }
+ .frame(maxWidth: .infinity)
}
}
@@ -44,7 +86,6 @@ struct RecommendationGroupView: View {
.listStyle(InsetListStyle())
#endif
}
- .task { await viewModel.loadGroups(dataService: dataService) }
}
private var shareView: some View {
@@ -55,6 +96,30 @@ struct RecommendationGroupView: View {
}
}
+ private var adminsSection: some View {
+ Section("Admins") {
+ ForEach(viewModel.recommendationGroup.admins) { admin in
+ SmallUserCard(data: ProfileCardData(
+ name: admin.name,
+ username: admin.username,
+ imageURL: admin.profileImageURL != nil ? URL(string: admin.profileImageURL!) : nil
+ ))
+ }
+ }
+ }
+
+ private var membersSection: some View {
+ Section("Members") {
+ ForEach(viewModel.nonAdmins) { member in
+ SmallUserCard(data: ProfileCardData(
+ name: member.name,
+ username: member.username,
+ imageURL: member.profileImageURL != nil ? URL(string: member.profileImageURL!) : nil
+ ))
+ }
+ }
+ }
+
private var innerBody: some View {
Group {
Section("Name") {
@@ -66,8 +131,12 @@ struct RecommendationGroupView: View {
presentShareSheet = true
}, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
+ .font(.appCaption)
})
}
+
+ adminsSection
+ membersSection
}
.formSheet(isPresented: $presentShareSheet) {
shareView
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
index 4164550fb..bc60ce80e 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift
@@ -6,7 +6,7 @@ import Utils
import Views
@MainActor final class CreateProfileViewModel: ObservableObject {
- private(set) var initialUserProfile = UserProfile(username: "", name: "", bio: nil)
+ private(set) var initialUserProfile = NewUserProfile(username: "", name: "", bio: nil)
var isConfigured = false
var hasSuggestedProfile: Bool {
@@ -31,7 +31,7 @@ import Views
init() {}
func submitProfile(name: String, bio: String, authenticator: Authenticator) {
- let profileOrError = UserProfile.make(
+ let profileOrError = NewUserProfile.make(
username: potentialUsername,
name: name,
bio: bio.isEmpty ? nil : bio
@@ -77,7 +77,7 @@ import Views
}
}
- func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
+ func submitProfile(userProfile: NewUserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
@@ -87,7 +87,7 @@ import Views
}
}
- func configure(profile: UserProfile, dataService: DataService) {
+ func configure(profile: NewUserProfile, dataService: DataService) {
guard !isConfigured else { return }
isConfigured = true
@@ -104,7 +104,7 @@ import Views
}
struct CreateProfileView: View {
- private let initialUserProfile: UserProfile
+ private let initialUserProfile: NewUserProfile
@State private var isConfigured = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@EnvironmentObject var authenticator: Authenticator
@@ -114,7 +114,7 @@ struct CreateProfileView: View {
@State private var name = ""
@State private var bio = ""
- init(userProfile: UserProfile) {
+ init(userProfile: NewUserProfile) {
self.initialUserProfile = userProfile
}
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
index 3bb561ac5..3eda742f1 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift
@@ -9,7 +9,7 @@ import Views
init() {}
- func submitProfile(userProfile: UserProfile, authenticator: Authenticator) async {
+ func submitProfile(userProfile: NewUserProfile, authenticator: Authenticator) async {
do {
try await authenticator.createAccount(userProfile: userProfile)
} catch {
@@ -23,7 +23,7 @@ import Views
struct NewAppleSignupView: View {
@EnvironmentObject var authenticator: Authenticator
@StateObject private var viewModel = NewAppleSignupViewModel()
- let userProfile: UserProfile
+ let userProfile: NewUserProfile
let showProfileEditView: () -> Void
var body: some View {
diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
index 8bd5754ab..db6250caf 100644
--- a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
+++ b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift
@@ -7,8 +7,8 @@ import Views
@MainActor final class RegistrationViewModel: ObservableObject {
enum RegistrationState {
- case createProfile(userProfile: UserProfile)
- case newAppleSignUp(userProfile: UserProfile)
+ case createProfile(userProfile: NewUserProfile)
+ case newAppleSignUp(userProfile: NewUserProfile)
}
@Published var loginError: LoginError?
@@ -65,7 +65,7 @@ import Views
case let .loginError(error):
loginError = error
case .newOmnivoreUser:
- registrationState = .createProfile(userProfile: UserProfile(username: "", name: ""))
+ registrationState = .createProfile(userProfile: NewUserProfile(username: "", name: ""))
case .existingOmnivoreUser:
break
}
diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
index 0e08359d7..9dbaf53aa 100644
--- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
+++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents
@@ -104,6 +104,14 @@
+
+
+
+
+
+
+
+
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift
new file mode 100644
index 000000000..a13c52bcb
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift
@@ -0,0 +1,52 @@
+import Foundation
+import Utils
+
+public struct NewUserProfile: Codable {
+ public let username: String
+ public let name: String
+ public let bio: String?
+
+ public init(
+ username: String,
+ name: String,
+ bio: String? = nil
+ ) {
+ self.username = username
+ self.name = name
+ self.bio = bio
+ }
+}
+
+public extension NewUserProfile {
+ static func make(
+ username: String,
+ name: String,
+ bio: String?
+ ) -> Either {
+ let userProfile = NewUserProfile(
+ username: username,
+ name: name,
+ bio: bio
+ )
+
+ if let errorMessage = userProfile.validationErrorMessage {
+ return .right(errorMessage)
+ } else {
+ return .left(userProfile)
+ }
+ }
+
+ private var validationErrorMessage: String? {
+ if name.isEmpty {
+ return "The name field is missing."
+ }
+
+ return nil
+ }
+}
+
+public extension Viewer {
+ var unwrappedUsername: String { username ?? "" }
+ var unwrappedName: String { name ?? "" }
+ var unwrappedUserID: String { userID ?? "" }
+}
diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
index 18e0a3da1..7c655577f 100644
--- a/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
+++ b/apple/OmnivoreKit/Sources/Models/DataModels/UserProfile.swift
@@ -1,52 +1,19 @@
+import CoreData
import Foundation
-import Utils
-
-public struct UserProfile: Codable {
- public let username: String
- public let name: String
- public let bio: String?
-
- public init(
- username: String,
- name: String,
- bio: String? = nil
- ) {
- self.username = username
- self.name = name
- self.bio = bio
- }
-}
public extension UserProfile {
- static func make(
- username: String,
- name: String,
- bio: String?
- ) -> Either {
- let userProfile = UserProfile(
- username: username,
- name: name,
- bio: bio
+ static func lookup(byID userID: String, inContext context: NSManagedObjectContext) -> UserProfile? {
+ let fetchRequest: NSFetchRequest = UserProfile.fetchRequest()
+ fetchRequest.predicate = NSPredicate(
+ format: "%K == %@", #keyPath(UserProfile.userID), userID
)
- if let errorMessage = userProfile.validationErrorMessage {
- return .right(errorMessage)
- } else {
- return .left(userProfile)
- }
- }
+ var result: UserProfile?
- private var validationErrorMessage: String? {
- if name.isEmpty {
- return "The name field is missing."
+ context.performAndWait {
+ result = (try? context.fetch(fetchRequest))?.first
}
- return nil
+ return result
}
}
-
-public extension Viewer {
- var unwrappedUsername: String { username ?? "" }
- var unwrappedName: String { name ?? "" }
- var unwrappedUserID: String { userID ?? "" }
-}
diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
index 73e31dbb7..1db37f136 100644
--- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
+++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift
@@ -5,12 +5,12 @@ public extension Authenticator {
func createPendingAccountUsingApple(
token: String,
name: PersonNameComponents?
- ) async throws -> UserProfile {
+ ) async throws -> NewUserProfile {
let params = CreatePendingAccountParams(token: token, provider: .apple, fullName: name)
return try await createPendingAccount(params: params)
}
- func createAccount(userProfile: UserProfile) async throws {
+ func createAccount(userProfile: NewUserProfile) async throws {
let params = CreateAccountParams(
pendingUserToken: pendingUserToken ?? "",
userProfile: userProfile
@@ -34,7 +34,7 @@ public extension Authenticator {
}
extension Authenticator {
- func createPendingAccount(params: CreatePendingAccountParams) async throws -> UserProfile {
+ func createPendingAccount(params: CreatePendingAccountParams) async throws -> NewUserProfile {
do {
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
let pendingUserAuthPayload = try await networker.createPendingUser(params: encodedParams)
diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
index 454e06b1e..550389c56 100644
--- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
+++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift
@@ -48,12 +48,12 @@ struct EmailAuthPayload: Decodable {
struct CreateAccountParams: Encodable {
let pendingUserToken: String
- let userProfile: UserProfile
+ let userProfile: NewUserProfile
}
struct PendingUserAuthPayload: Decodable {
let pendingUserToken: String
- let pendingUserProfile: UserProfile
+ let pendingUserProfile: NewUserProfile
}
struct PendingEmailVerificationAuthPayload: Decodable {
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift
index 4064c1bf9..7759f9814 100644
--- a/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/RecommendationGroupSelection.swift
@@ -5,6 +5,8 @@ let recommendationGroupSelection = Selection.RecommendationGroup {
InternalRecommendationGroup(
id: try $0.id(),
name: try $0.name(),
- inviteUrl: try $0.inviteUrl()
+ inviteUrl: try $0.inviteUrl(),
+ admins: try $0.admins(selection: userProfileSelection.list),
+ members: try $0.members(selection: userProfileSelection.list)
)
}
diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift
new file mode 100644
index 000000000..ad9562955
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/UserProfileSelection.swift
@@ -0,0 +1,18 @@
+import Models
+import SwiftGraphQL
+
+let profileSelection = Selection.Profile {
+ (
+ username: try $0.username(),
+ profileImageURL: try $0.pictureUrl()
+ )
+}
+
+let userProfileSelection = Selection.User {
+ InternalUserProfile(
+ userID: try $0.id(),
+ name: try $0.name(),
+ username: try $0.profile(selection: profileSelection).username,
+ profileImageURL: try $0.profile(selection: profileSelection).profileImageURL
+ )
+}
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
index c787cc1f8..ff90cc253 100644
--- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalRecommendationGroup.swift
@@ -9,10 +9,12 @@ import CoreData
import Foundation
import Models
-public struct InternalRecommendationGroup: Encodable, Identifiable {
+public struct InternalRecommendationGroup: Identifiable {
public let id: String
public let name: String
public let inviteUrl: String
+ public let admins: [InternalUserProfile]
+ public let members: [InternalUserProfile]
func asManagedObject(inContext context: NSManagedObjectContext) -> RecommendationGroup {
let fetchRequest: NSFetchRequest = RecommendationGroup.fetchRequest()
@@ -37,7 +39,9 @@ public struct InternalRecommendationGroup: Encodable, Identifiable {
return InternalRecommendationGroup(
id: id,
name: name,
- inviteUrl: inviteUrl
+ inviteUrl: inviteUrl,
+ admins: InternalUserProfile.make(recommendationGroup.admins),
+ members: InternalUserProfile.make(recommendationGroup.members)
)
}
return nil
diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift
new file mode 100644
index 000000000..75a2924a8
--- /dev/null
+++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalUserProfile.swift
@@ -0,0 +1,63 @@
+import CoreData
+import Foundation
+import Models
+
+public struct InternalUserProfile: Identifiable {
+ let userID: String
+ public let name: String
+ public let username: String
+ public let profileImageURL: String?
+
+ public var id: String {
+ userID
+ }
+
+ func persist(context: NSManagedObjectContext) -> NSManagedObjectID? {
+ var objectID: NSManagedObjectID?
+
+ context.performAndWait {
+ let user = asManagedObject(inContext: context)
+
+ do {
+ try context.save()
+ logger.debug("User saved succesfully")
+ objectID = user.objectID
+ } catch {
+ context.rollback()
+ logger.debug("Failed to save User: \(error.localizedDescription)")
+ }
+ }
+
+ return objectID
+ }
+
+ func asManagedObject(inContext context: NSManagedObjectContext) -> UserProfile {
+ let existing = UserProfile.lookup(byID: userID, inContext: context)
+ let userProfile = existing ?? UserProfile(entity: UserProfile.entity(), insertInto: context)
+
+ userProfile.userID = userID
+ userProfile.name = name
+ userProfile.username = username
+ userProfile.profileImageURL = profileImageURL
+ return userProfile
+ }
+
+ public static func make(_ users: NSSet?) -> [InternalUserProfile] {
+ users?
+ .compactMap { user in
+ if let user = user as? UserProfile,
+ let userID = user.userID,
+ let name = user.name,
+ let username = user.username
+ {
+ return InternalUserProfile(
+ userID: userID,
+ name: name,
+ username: username,
+ profileImageURL: user.profileImageURL
+ )
+ }
+ return nil
+ } ?? []
+ }
+}
diff --git a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
index 9e923a560..dc1dc69aa 100644
--- a/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
+++ b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift
@@ -1,9 +1,9 @@
import SwiftUI
public struct ProfileCardData {
- let name: String
- let username: String
- let imageURL: URL?
+ public let name: String
+ public let username: String
+ public let imageURL: URL?
public init(name: String = "", username: String = "", imageURL: URL? = nil) {
self.name = name