mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add rough UI for recommendation groups
This commit is contained in:
parent
55d4c89754
commit
bdbad81fa9
9 changed files with 2601 additions and 0 deletions
|
|
@ -107,6 +107,12 @@ struct ProfileView: View {
|
|||
}
|
||||
#endif
|
||||
|
||||
Section {
|
||||
NavigationLink(destination: GroupsView()) {
|
||||
Text("Recommendation Groups")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink(
|
||||
destination: BasicWebAppView.privacyPolicyWebView(baseURL: dataService.appEnvironment.webAppBaseURL)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Views
|
||||
|
||||
@MainActor final class RecommendationsGroupViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var networkError = true
|
||||
@Published var recommendationGroup: InternalRecommendationGroup
|
||||
|
||||
init(recommendationGroup: InternalRecommendationGroup) {
|
||||
self.recommendationGroup = recommendationGroup
|
||||
}
|
||||
|
||||
func loadGroups(dataService _: DataService) async {
|
||||
isLoading = true
|
||||
|
||||
// do {
|
||||
// recommendationGroups = try await dataService.recommendationGroups()
|
||||
// } catch {
|
||||
// networkError = true
|
||||
// }
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
struct RecommendationGroupView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var viewModel: RecommendationsGroupViewModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Form {
|
||||
innerBody
|
||||
}
|
||||
#elseif os(macOS)
|
||||
List {
|
||||
innerBody
|
||||
}
|
||||
.listStyle(InsetListStyle())
|
||||
#endif
|
||||
}
|
||||
.task { await viewModel.loadGroups(dataService: dataService) }
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
Section("Name") {
|
||||
Text(viewModel.recommendationGroup.name)
|
||||
}
|
||||
|
||||
Section("Invite Link") {
|
||||
Button(action: {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = viewModel.recommendationGroup.inviteUrl
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
let pasteBoard = NSPasteboard.general
|
||||
pasteBoard.clearContents()
|
||||
pasteBoard.writeObjects([viewModel.recommendationGroup.inviteUrl as NSString])
|
||||
#endif
|
||||
|
||||
Snackbar.show(message: "Invite link copied")
|
||||
}, label: {
|
||||
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
|
||||
})
|
||||
}
|
||||
}
|
||||
.navigationTitle(viewModel.recommendationGroup.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Views
|
||||
|
||||
@MainActor final class RecommendationsGroupsViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var isCreating = false
|
||||
@Published var networkError = true
|
||||
@Published var recommendationGroups = [InternalRecommendationGroup]()
|
||||
|
||||
@Published var showCreateSheet = false
|
||||
|
||||
@Published var showCreateError = false
|
||||
@Published var createGroupError: String?
|
||||
|
||||
func loadGroups(dataService: DataService) async {
|
||||
isLoading = true
|
||||
|
||||
do {
|
||||
recommendationGroups = try await dataService.recommendationGroups()
|
||||
} catch {
|
||||
networkError = true
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func createGroup(dataService: DataService, name: String) async {
|
||||
isCreating = true
|
||||
|
||||
if let group = try? await dataService.createRecommendationGroup(name: name) {
|
||||
print("CREATED GROUP: ", group)
|
||||
await loadGroups(dataService: dataService)
|
||||
showCreateSheet = false
|
||||
} else {
|
||||
createGroupError = "Error creating group"
|
||||
showCreateError = true
|
||||
}
|
||||
|
||||
isCreating = false
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateRecommendationGroupView: View {
|
||||
@State var name = ""
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var viewModel = RecommendationsGroupsViewModel()
|
||||
|
||||
var nextButton: some View {
|
||||
if viewModel.isCreating {
|
||||
return AnyView(ProgressView())
|
||||
} else {
|
||||
return AnyView(Button(action: {
|
||||
Task {
|
||||
await viewModel.createGroup(dataService: dataService, name: self.name)
|
||||
}
|
||||
}, label: {
|
||||
Text("Next")
|
||||
})
|
||||
.disabled(name.isEmpty)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
TextField("Name", text: $name, prompt: Text("Group Name"))
|
||||
}
|
||||
.alert(isPresented: $viewModel.showCreateError) {
|
||||
Alert(
|
||||
title: Text(viewModel.createGroupError ?? "Error creating group"),
|
||||
dismissButton: .cancel(Text("Ok")) {
|
||||
viewModel.createGroupError = nil
|
||||
viewModel.showCreateError = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.navigationTitle("Create Group")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(leading:
|
||||
Button(action: {
|
||||
viewModel.showCreateSheet = false
|
||||
}, label: { Text("Cancel") }),
|
||||
trailing: nextButton)
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupsView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var viewModel = RecommendationsGroupsViewModel()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Form {
|
||||
innerBody
|
||||
}
|
||||
#elseif os(macOS)
|
||||
List {
|
||||
innerBody
|
||||
}
|
||||
.listStyle(InsetListStyle())
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showCreateSheet) {
|
||||
NavigationView {
|
||||
CreateRecommendationGroupView(viewModel: self.viewModel)
|
||||
}
|
||||
}
|
||||
.task { await viewModel.loadGroups(dataService: dataService) }
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
Section {
|
||||
Button(
|
||||
action: { viewModel.showCreateSheet = true },
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill").foregroundColor(.green)
|
||||
Text("Create a new group")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
|
||||
Section(header: Text("Your recommendation groups")) {
|
||||
ForEach(viewModel.recommendationGroups) { recommendationGroup in
|
||||
NavigationLink(
|
||||
destination: RecommendationGroupView(viewModel: RecommendationsGroupViewModel(recommendationGroup: recommendationGroup))
|
||||
) {
|
||||
Text(recommendationGroup.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Recommendation Groups")
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +91,13 @@
|
|||
<attribute name="savedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="term" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="RecommendationGroup" representedClassName="RecommendationGroup" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="inviteUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<entity name="Viewer" representedClassName="Viewer" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="profileImageURL" optional="YES" attributeType="String"/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,53 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func createRecommendationGroup(name: String) async throws -> InternalRecommendationGroup {
|
||||
enum MutationResult {
|
||||
case saved(recommendationGroup: InternalRecommendationGroup)
|
||||
case error(errorCode: Enums.CreateGroupErrorCode)
|
||||
}
|
||||
|
||||
let selection = Selection<MutationResult, Unions.CreateGroupResult> {
|
||||
try $0.on(
|
||||
createGroupError: .init {
|
||||
.error(errorCode: try $0.errorCodes().first ?? .badRequest)
|
||||
},
|
||||
createGroupSuccess: .init {
|
||||
.saved(recommendationGroup: try $0.group(selection: recommendationGroupSelection))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let input = InputObjects.CreateGroupInput(name: name)
|
||||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.createGroup(input: input, selection: selection)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(mutation, to: path, headers: headers) { [weak self] queryResult in
|
||||
guard let payload = try? queryResult.get(), let self = self else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "network error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case let .saved(recommendationGroup: recommendationGroup):
|
||||
if [recommendationGroup].persist(context: self.backgroundContext) != nil {
|
||||
continuation.resume(returning: recommendationGroup)
|
||||
} else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
|
||||
}
|
||||
case let .error(errorCode: errorCode):
|
||||
continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func recommendationGroups() async throws -> [InternalRecommendationGroup] {
|
||||
enum QueryResult {
|
||||
case success(result: [InternalRecommendationGroup])
|
||||
case error(error: String)
|
||||
}
|
||||
|
||||
let selection = Selection<QueryResult, Unions.GroupsResult> {
|
||||
try $0.on(
|
||||
groupsError: .init {
|
||||
QueryResult.error(error: try $0.errorCodes().description)
|
||||
},
|
||||
groupsSuccess: .init {
|
||||
QueryResult.success(result: try $0.groups(selection: recommendationGroupSelection.list))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let query = Selection.Query {
|
||||
try $0.groups(selection: selection)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
let context = backgroundContext
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(query, to: path, headers: headers) { queryResult in
|
||||
guard let payload = try? queryResult.get() else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "network request failed"))
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case let .success(result: result):
|
||||
if result.persist(context: context) != nil {
|
||||
continuation.resume(returning: result)
|
||||
} else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "CoreData error"))
|
||||
}
|
||||
case .error:
|
||||
continuation.resume(throwing: BasicError.message(messageText: "Recommendation Groups fetch error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
let recommendationGroupSelection = Selection.RecommendationGroup {
|
||||
InternalRecommendationGroup(
|
||||
id: try $0.id(),
|
||||
name: try $0.name(),
|
||||
inviteUrl: try $0.inviteUrl()
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// InternalRecommendationGroup.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 12/5/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
|
||||
public struct InternalRecommendationGroup: Encodable, Identifiable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let inviteUrl: String
|
||||
|
||||
func asManagedObject(inContext context: NSManagedObjectContext) -> RecommendationGroup {
|
||||
let fetchRequest: NSFetchRequest<Models.RecommendationGroup> = RecommendationGroup.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", id
|
||||
)
|
||||
let existing = (try? context.fetch(fetchRequest))?.first
|
||||
let recommendationGroup = existing ?? RecommendationGroup(entity: RecommendationGroup.entity(), insertInto: context)
|
||||
|
||||
recommendationGroup.id = id
|
||||
recommendationGroup.name = name
|
||||
recommendationGroup.inviteUrl = inviteUrl
|
||||
|
||||
return recommendationGroup
|
||||
}
|
||||
|
||||
static func make(from recommendationGroup: RecommendationGroup) -> InternalRecommendationGroup? {
|
||||
if let id = recommendationGroup.id,
|
||||
let name = recommendationGroup.name,
|
||||
let inviteUrl = recommendationGroup.inviteUrl
|
||||
{
|
||||
return InternalRecommendationGroup(
|
||||
id: id,
|
||||
name: name,
|
||||
inviteUrl: inviteUrl
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Element == InternalRecommendationGroup {
|
||||
func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? {
|
||||
var result: [NSManagedObjectID]?
|
||||
|
||||
context.performAndWait {
|
||||
let recommendationGroups = map { $0.asManagedObject(inContext: context) }
|
||||
do {
|
||||
try context.save()
|
||||
logger.debug("InternalRecommendationGroup saved succesfully")
|
||||
result = recommendationGroups.map(\.objectID)
|
||||
} catch {
|
||||
context.rollback()
|
||||
logger.debug("Failed to save InternalRecommendationGroups: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue