Display members and admins in Recommendation Groups

This commit is contained in:
Jackson Harper 2022-12-06 13:35:46 +08:00
parent 19a5495bd3
commit f4b101a12f
14 changed files with 256 additions and 73 deletions

View file

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

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -104,6 +104,14 @@
<attribute name="inviteUrl" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="admins" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Viewer"/>
<relationship name="members" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Viewer"/>
</entity>
<entity name="UserProfile" representedClassName="UserProfile" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="profileImageURL" optional="YES" attributeType="String"/>
<attribute name="userID" optional="YES" attributeType="String"/>
<attribute name="username" optional="YES" attributeType="String"/>
</entity>
<entity name="Viewer" representedClassName="Viewer" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String"/>

View file

@ -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<NewUserProfile, String> {
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 ?? "" }
}

View file

@ -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<UserProfile, String> {
let userProfile = UserProfile(
username: username,
name: name,
bio: bio
static func lookup(byID userID: String, inContext context: NSManagedObjectContext) -> UserProfile? {
let fetchRequest: NSFetchRequest<Models.UserProfile> = 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 ?? "" }
}

View file

@ -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)

View file

@ -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 {

View file

@ -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)
)
}

View file

@ -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
)
}

View file

@ -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<Models.RecommendationGroup> = 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

View file

@ -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
} ?? []
}
}

View file

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