mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Display members and admins in Recommendation Groups
This commit is contained in:
parent
19a5495bd3
commit
f4b101a12f
14 changed files with 256 additions and 73 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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 ?? "" }
|
||||
}
|
||||
|
|
@ -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 ?? "" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue