From f97c196f3e6e4e6fcd067d3a563450ae43b99237 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 14:29:39 -0800 Subject: [PATCH 01/21] extract welcome view components into structs --- .../Views/WelcomeView/WelcomeView.swift | 125 ++++++++++-------- 1 file changed, 72 insertions(+), 53 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift b/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift index a85d46a16..a9ae1c09a 100644 --- a/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift @@ -36,62 +36,13 @@ public struct WelcomeView: View { if showRegistrationView { RegistrationView(viewModel: viewModel.registrationViewModel) } else { - getStartedView + GetStartedView(showRegistrationView: $showRegistrationView) } } .frame(width: width) .zIndex(2) } - var titleLogo: some View { - Image.omnivoreTitleLogo - .renderingMode(.template) - .foregroundColor(.appGrayTextContrast) - .frame(height: 40) - .gesture( - TapGesture(count: 2) - .onEnded { - viewModel.performActionSubject.send(.hiddenGesturePerformed) - } - ) - } - - var getStartedView: some View { - HStack { - VStack(alignment: .leading, spacing: 32) { - Text("A better social\nreading experience\nstarts with Omnivore.") - .font(.appTitle) - .multilineTextAlignment(.leading) - - BorderedButton(color: .appGrayTextContrast, text: "Get Started") { - showRegistrationView = true - } - .frame(width: 220) - } - .padding(.leading, horizontalSizeClass == .compact ? 16 : 80) - .padding(.top, horizontalSizeClass == .compact ? 16 : 0) - - Spacer() - } - } - - @ViewBuilder func splitColorBackground(width: CGFloat) -> some View { - HStack(spacing: 0) { - Color.systemBackground.frame(width: width * 0.5) - Color.appBackground.frame(width: width * 0.5) - } - .edgesIgnoringSafeArea(.all) - } - - @ViewBuilder func largeBackgroundImage(width: CGFloat) -> some View { - Image.readingIllustrationXXL - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: width) - .clipped() - .edgesIgnoringSafeArea([.vertical, .trailing]) - } - @ViewBuilder func primaryContent() -> some View { if horizontalSizeClass == .compact { GeometryReader { geometry in @@ -121,17 +72,19 @@ public struct WelcomeView: View { } else { GeometryReader { geometry in ZStack(alignment: .leading) { - splitColorBackground(width: geometry.size.width) + SplitColorBackground(width: geometry.size.width) VStack { - titleLogo + TitleLogoView { + viewModel.performActionSubject.send(.hiddenGesturePerformed) + } Spacer() } .padding() HStack(spacing: 0) { userInteractiveView(width: geometry.size.width * 0.5) - largeBackgroundImage(width: geometry.size.width * 0.5) + ReadingIllustrationXXLView(width: geometry.size.width * 0.5) } } } @@ -148,3 +101,69 @@ public struct WelcomeView: View { .onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 } } } + +public struct ReadingIllustrationXXLView: View { + let width: CGFloat + + public var body: some View { + Image.readingIllustrationXXL + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: width) + .clipped() + .edgesIgnoringSafeArea([.vertical, .trailing]) + } +} + +public struct TitleLogoView: View { + let handleHiddenGestureAction: () -> Void + + public var body: some View { + Image.omnivoreTitleLogo + .renderingMode(.template) + .foregroundColor(.appGrayTextContrast) + .frame(height: 40) + .gesture( + TapGesture(count: 2) + .onEnded { + handleHiddenGestureAction() + } + ) + } +} + +struct GetStartedView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Binding var showRegistrationView: Bool + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 32) { + Text("A better social\nreading experience\nstarts with Omnivore.") + .font(.appTitle) + .multilineTextAlignment(.leading) + + BorderedButton(color: .appGrayTextContrast, text: "Get Started") { + showRegistrationView = true + } + .frame(width: 220) + } + .padding(.leading, horizontalSizeClass == .compact ? 16 : 80) + .padding(.top, horizontalSizeClass == .compact ? 16 : 0) + + Spacer() + } + } +} + +struct SplitColorBackground: View { + let width: CGFloat + + var body: some View { + HStack(spacing: 0) { + Color.systemBackground.frame(width: width * 0.5) + Color.appBackground.frame(width: width * 0.5) + } + .edgesIgnoringSafeArea(.all) + } +} From aa355088d21ed4ace2a3707c7ca18644a16e230e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 14:50:25 -0800 Subject: [PATCH 02/21] remove welcome view model --- .../Sources/Binders/RootViewModel.swift | 2 +- .../Sources/Binders/Scenes/WelcomeScene.swift | 26 --- .../Sources/Binders/Views/WelcomeView.swift | 83 +++++++++ .../Sources/Views/KeyboardManagement.swift | 2 +- .../RegistrationHeroImageView.swift | 8 +- .../Views/WelcomeView/WelcomeView.swift | 169 ------------------ .../WelcomeView/WelcomeViewComponents.swift | 83 +++++++++ 7 files changed, 174 insertions(+), 199 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/WelcomeScene.swift create mode 100644 apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift create mode 100644 apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeViewComponents.swift diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index f64616d46..9259d3f65 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -170,7 +170,7 @@ public struct RootView: View { #endif } else { - WelcomeView(viewModel: WelcomeViewModel.make(services: viewModel.services)) + WelcomeView(services: viewModel.services) .accessibilityElement() .accessibilityIdentifier("welcomeView") } diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/WelcomeScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/WelcomeScene.swift deleted file mode 100644 index f9fcf48a3..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/WelcomeScene.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Services -import SwiftUI -import Utils -import Views - -extension WelcomeViewModel { - static func make(services: Services) -> WelcomeViewModel { - let registrationViewModel = RegistrationViewModel.make(services: services) - let viewModel = WelcomeViewModel(registrationViewModel: registrationViewModel) - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case .hiddenGesturePerformed: - if !Bundle.main.isAppStoreBuild { - self?.debugMenuViewModel = DebugMenuViewModel.make(services: services) - self?.showDebugModal = true - } - } - } - .store(in: &subscriptions) - } -} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift new file mode 100644 index 000000000..9d80f2283 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift @@ -0,0 +1,83 @@ +import Combine +import Services +import SwiftUI +import Utils +import Views + +struct WelcomeView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + let services: Services + @State private var showRegistrationView = false + @State private var isKeyboardOnScreen = false + @State private var showDebugModal = false + + func handleHiddenGestureAction() { + if !Bundle.main.isAppStoreBuild { + showDebugModal = true + } + } + + @ViewBuilder func userInteractiveView(width: CGFloat) -> some View { + Group { + if showRegistrationView { + RegistrationView(viewModel: RegistrationViewModel.make(services: services)) + } else { + GetStartedView(showRegistrationView: $showRegistrationView) + } + } + .frame(width: width) + .zIndex(2) + } + + @ViewBuilder func primaryContent() -> some View { + if horizontalSizeClass == .compact { + GeometryReader { geometry in + ZStack(alignment: .leading) { + Color.systemBackground + .edgesIgnoringSafeArea(.all) + + if geometry.size.width < geometry.size.height, !isKeyboardOnScreen { + VStack { + Color.appDeepBackground.frame(height: 100) + Spacer() + } + .edgesIgnoringSafeArea(.all) + } + + VStack { + if geometry.size.width < geometry.size.height, !isKeyboardOnScreen { + RegistrationHeroImageView(tapGestureHandler: handleHiddenGestureAction) + } + userInteractiveView(width: geometry.size.width) + Spacer() + } + } + } + } else { + GeometryReader { geometry in + ZStack(alignment: .leading) { + SplitColorBackground(width: geometry.size.width) + + VStack { + TitleLogoView(handleHiddenGestureAction: handleHiddenGestureAction) + Spacer() + } + .padding() + + HStack(spacing: 0) { + userInteractiveView(width: geometry.size.width * 0.5) + ReadingIllustrationXXLView(width: geometry.size.width * 0.5) + } + } + } + } + } + + public var body: some View { + primaryContent() + .sheet(isPresented: $showDebugModal) { + DebugMenuView(viewModel: DebugMenuViewModel.make(services: services)) + } + .onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/KeyboardManagement.swift b/apple/OmnivoreKit/Sources/Views/KeyboardManagement.swift index bbac86b51..ca0e42a3a 100644 --- a/apple/OmnivoreKit/Sources/Views/KeyboardManagement.swift +++ b/apple/OmnivoreKit/Sources/Views/KeyboardManagement.swift @@ -5,7 +5,7 @@ import SwiftUI import UIKit #endif -extension Publishers { +public extension Publishers { static var keyboardHeight: AnyPublisher { #if os(iOS) let willShow = NotificationCenter.default diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationHeroImageView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationHeroImageView.swift index 5f8bccb50..e57096d91 100644 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationHeroImageView.swift +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationHeroImageView.swift @@ -1,9 +1,13 @@ import SwiftUI -struct RegistrationHeroImageView: View { +public struct RegistrationHeroImageView: View { let tapGestureHandler: () -> Void - var body: some View { + public init(tapGestureHandler: @escaping () -> Void) { + self.tapGestureHandler = tapGestureHandler + } + + public var body: some View { ZStack(alignment: .topLeading) { Image.readingIllustration .resizable() diff --git a/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift b/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift deleted file mode 100644 index a9ae1c09a..000000000 --- a/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeView.swift +++ /dev/null @@ -1,169 +0,0 @@ -import Combine -import Models -import SwiftUI - -public final class WelcomeViewModel: ObservableObject { - @Published public var showDebugModal: Bool = false - @Published public var showRegistrationModal: Bool = false - - public let registrationViewModel: RegistrationViewModel - public var debugMenuViewModel: DebugMenuViewModel? - - public enum Action { - case hiddenGesturePerformed - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init(registrationViewModel: RegistrationViewModel) { - self.registrationViewModel = registrationViewModel - } -} - -public struct WelcomeView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var viewModel: WelcomeViewModel - @State private var showRegistrationView = false - @State private var isKeyboardOnScreen = false - - public init(viewModel: WelcomeViewModel) { - self.viewModel = viewModel - } - - @ViewBuilder func userInteractiveView(width: CGFloat) -> some View { - Group { - if showRegistrationView { - RegistrationView(viewModel: viewModel.registrationViewModel) - } else { - GetStartedView(showRegistrationView: $showRegistrationView) - } - } - .frame(width: width) - .zIndex(2) - } - - @ViewBuilder func primaryContent() -> some View { - if horizontalSizeClass == .compact { - GeometryReader { geometry in - ZStack(alignment: .leading) { - Color.systemBackground - .edgesIgnoringSafeArea(.all) - - if geometry.size.width < geometry.size.height, !isKeyboardOnScreen { - VStack { - Color.appDeepBackground.frame(height: 100) - Spacer() - } - .edgesIgnoringSafeArea(.all) - } - - VStack { - if geometry.size.width < geometry.size.height, !isKeyboardOnScreen { - RegistrationHeroImageView( - tapGestureHandler: { viewModel.performActionSubject.send(.hiddenGesturePerformed) } - ) - } - userInteractiveView(width: geometry.size.width) - Spacer() - } - } - } - } else { - GeometryReader { geometry in - ZStack(alignment: .leading) { - SplitColorBackground(width: geometry.size.width) - - VStack { - TitleLogoView { - viewModel.performActionSubject.send(.hiddenGesturePerformed) - } - Spacer() - } - .padding() - - HStack(spacing: 0) { - userInteractiveView(width: geometry.size.width * 0.5) - ReadingIllustrationXXLView(width: geometry.size.width * 0.5) - } - } - } - } - } - - public var body: some View { - primaryContent() - .sheet(isPresented: $viewModel.showDebugModal) { - if let debugMenuViewModel = viewModel.debugMenuViewModel { - DebugMenuView(viewModel: debugMenuViewModel) - } - } - .onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 } - } -} - -public struct ReadingIllustrationXXLView: View { - let width: CGFloat - - public var body: some View { - Image.readingIllustrationXXL - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: width) - .clipped() - .edgesIgnoringSafeArea([.vertical, .trailing]) - } -} - -public struct TitleLogoView: View { - let handleHiddenGestureAction: () -> Void - - public var body: some View { - Image.omnivoreTitleLogo - .renderingMode(.template) - .foregroundColor(.appGrayTextContrast) - .frame(height: 40) - .gesture( - TapGesture(count: 2) - .onEnded { - handleHiddenGestureAction() - } - ) - } -} - -struct GetStartedView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Binding var showRegistrationView: Bool - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 32) { - Text("A better social\nreading experience\nstarts with Omnivore.") - .font(.appTitle) - .multilineTextAlignment(.leading) - - BorderedButton(color: .appGrayTextContrast, text: "Get Started") { - showRegistrationView = true - } - .frame(width: 220) - } - .padding(.leading, horizontalSizeClass == .compact ? 16 : 80) - .padding(.top, horizontalSizeClass == .compact ? 16 : 0) - - Spacer() - } - } -} - -struct SplitColorBackground: View { - let width: CGFloat - - var body: some View { - HStack(spacing: 0) { - Color.systemBackground.frame(width: width * 0.5) - Color.appBackground.frame(width: width * 0.5) - } - .edgesIgnoringSafeArea(.all) - } -} diff --git a/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeViewComponents.swift b/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeViewComponents.swift new file mode 100644 index 000000000..f4532c1b9 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/WelcomeView/WelcomeViewComponents.swift @@ -0,0 +1,83 @@ +import SwiftUI + +public struct ReadingIllustrationXXLView: View { + let width: CGFloat + + public init(width: CGFloat) { + self.width = width + } + + public var body: some View { + Image.readingIllustrationXXL + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: width) + .clipped() + .edgesIgnoringSafeArea([.vertical, .trailing]) + } +} + +public struct TitleLogoView: View { + let handleHiddenGestureAction: () -> Void + + public init(handleHiddenGestureAction: @escaping () -> Void) { + self.handleHiddenGestureAction = handleHiddenGestureAction + } + + public var body: some View { + Image.omnivoreTitleLogo + .renderingMode(.template) + .foregroundColor(.appGrayTextContrast) + .frame(height: 40) + .gesture( + TapGesture(count: 2) + .onEnded { + handleHiddenGestureAction() + } + ) + } +} + +public struct GetStartedView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Binding var showRegistrationView: Bool + + public init(showRegistrationView: Binding) { + self._showRegistrationView = showRegistrationView + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 32) { + Text("A better social\nreading experience\nstarts with Omnivore.") + .font(.appTitle) + .multilineTextAlignment(.leading) + + BorderedButton(color: .appGrayTextContrast, text: "Get Started") { + showRegistrationView = true + } + .frame(width: 220) + } + .padding(.leading, horizontalSizeClass == .compact ? 16 : 80) + .padding(.top, horizontalSizeClass == .compact ? 16 : 0) + + Spacer() + } + } +} + +public struct SplitColorBackground: View { + let width: CGFloat + + public init(width: CGFloat) { + self.width = width + } + + public var body: some View { + HStack(spacing: 0) { + Color.systemBackground.frame(width: width * 0.5) + Color.appBackground.frame(width: width * 0.5) + } + .edgesIgnoringSafeArea(.all) + } +} From 677a31577843f599063f7a48fb1696eced06c698 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 14:59:34 -0800 Subject: [PATCH 03/21] move DebugMenuView to binders package --- .../Binders/Scenes/DebugMenuScene.swift | 76 +++++++++++++++++++ .../Sources/Views/Buttons/ButtonStyles.swift | 6 +- .../Sources/Views/DebugMenuView.swift | 76 ------------------- .../RegistrationViewModel.swift | 2 - 4 files changed, 79 insertions(+), 81 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift index fd0ae8fb5..0d801befe 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift @@ -1,3 +1,5 @@ +import Combine +import Models import Services import SwiftUI import Views @@ -28,3 +30,77 @@ extension DebugMenuViewModel { .store(in: &subscriptions) } } + +public final class DebugMenuViewModel { + public enum Action { + case applyChanges(environment: DebugMenuEnvOption) + } + + let initialEnvironment: AppEnvironment + public var subscriptions = Set() + public let performActionSubject = PassthroughSubject() + + public init(initialEnvironment: AppEnvironment) { + self.initialEnvironment = initialEnvironment + } + + var initialDebugMenuEnvOption: DebugMenuEnvOption { + DebugMenuEnvOption.make(from: initialEnvironment) + } +} + +public enum DebugMenuEnvOption: String, CaseIterable { + case dev = "Dev" + case prod = "Prod" + case demo = "Demo" + case local = "Local" + + static func make(from appEnvironment: AppEnvironment) -> DebugMenuEnvOption { + switch appEnvironment { + case .local: + return .local + case .dev: + return .dev + case .prod: + return .prod + case .demo: + return .demo + case .test: + return .local + } + } +} + +public struct DebugMenuView: View { + @State private var selectedEnvironment: DebugMenuEnvOption + + private var viewModel: DebugMenuViewModel + + public init(viewModel: DebugMenuViewModel) { + self.viewModel = viewModel + self._selectedEnvironment = State(initialValue: viewModel.initialDebugMenuEnvOption) + } + + public var body: some View { + VStack { + Text("Debug Menu") + .font(.appTitle) + Form { + Text("API Environment:") + Picker(selection: $selectedEnvironment, label: Text("API Environment:")) { + ForEach(DebugMenuEnvOption.allCases, id: \.self) { + Text($0.rawValue) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + + Button( + action: { viewModel.performActionSubject.send(.applyChanges(environment: selectedEnvironment)) }, + label: { Text("Apply Changes") } + ) + .buttonStyle(SolidCapsuleButtonStyle(width: 220)) + } + .padding() + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift index 634d74045..64f72fcb7 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift @@ -1,17 +1,17 @@ import SwiftUI -struct SolidCapsuleButtonStyle: ButtonStyle { +public struct SolidCapsuleButtonStyle: ButtonStyle { let backgroundColor: Color let textColor: Color let width: CGFloat - init(color: Color = .blue, textColor: Color = .appGrayTextContrast, width: CGFloat = 220) { + public init(color: Color = .blue, textColor: Color = .appGrayTextContrast, width: CGFloat = 220) { self.backgroundColor = color self.textColor = textColor self.width = width } - func makeBody(configuration: Configuration) -> some View { + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.appBody) .foregroundColor(textColor) diff --git a/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift index c62b471ba..8b1378917 100644 --- a/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift +++ b/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift @@ -1,77 +1 @@ -import Combine -import Models -import SwiftUI -public final class DebugMenuViewModel { - public enum Action { - case applyChanges(environment: DebugMenuEnvOption) - } - - let initialEnvironment: AppEnvironment - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init(initialEnvironment: AppEnvironment) { - self.initialEnvironment = initialEnvironment - } - - var initialDebugMenuEnvOption: DebugMenuEnvOption { - DebugMenuEnvOption.make(from: initialEnvironment) - } -} - -public enum DebugMenuEnvOption: String, CaseIterable { - case dev = "Dev" - case prod = "Prod" - case demo = "Demo" - case local = "Local" - - static func make(from appEnvironment: AppEnvironment) -> DebugMenuEnvOption { - switch appEnvironment { - case .local: - return .local - case .dev: - return .dev - case .prod: - return .prod - case .demo: - return .demo - case .test: - return .local - } - } -} - -public struct DebugMenuView: View { - @State private var selectedEnvironment: DebugMenuEnvOption - - private var viewModel: DebugMenuViewModel - - public init(viewModel: DebugMenuViewModel) { - self.viewModel = viewModel - self._selectedEnvironment = State(initialValue: viewModel.initialDebugMenuEnvOption) - } - - public var body: some View { - VStack { - Text("Debug Menu") - .font(.appTitle) - Form { - Text("API Environment:") - Picker(selection: $selectedEnvironment, label: Text("API Environment:")) { - ForEach(DebugMenuEnvOption.allCases, id: \.self) { - Text($0.rawValue) - } - } - .pickerStyle(SegmentedPickerStyle()) - } - - Button( - action: { viewModel.performActionSubject.send(.applyChanges(environment: selectedEnvironment)) }, - label: { Text("Apply Changes") } - ) - .buttonStyle(SolidCapsuleButtonStyle(width: 220)) - } - .padding() - } -} diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift index 1843ec0c7..9fc277e2a 100644 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift @@ -13,8 +13,6 @@ public final class RegistrationViewModel: ObservableObject { @Published public var createProfileViewModel: CreateProfileViewModel? @Published public var newAppleSignupViewModel: NewAppleSignupViewModel? - public var debugMenuViewModel: DebugMenuViewModel? - public enum Action { case googleButtonTapped case appleSignInCompleted(result: Result) From 6e0611103551104923f5637be90236564fb36195 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:15:44 -0800 Subject: [PATCH 04/21] remove DebugMenuViewModel --- .../Binders/Scenes/DebugMenuScene.swift | 106 ------------------ .../Sources/Binders/Views/DebugMenuView.swift | 38 +++++++ .../Sources/Binders/Views/WelcomeView.swift | 2 +- .../Sources/Views/DebugMenuView.swift | 1 - 4 files changed, 39 insertions(+), 108 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift create mode 100644 apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/DebugMenuView.swift diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift deleted file mode 100644 index 0d801befe..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/DebugMenuScene.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Combine -import Models -import Services -import SwiftUI -import Views - -extension DebugMenuViewModel { - static func make(services: Services) -> DebugMenuViewModel { - let viewModel = DebugMenuViewModel(initialEnvironment: services.dataService.appEnvironment) - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { action in - switch action { - case let .applyChanges(environment): - switch environment { - case .dev: - services.switchAppEnvironment(to: .dev) - case .demo: - services.switchAppEnvironment(to: .demo) - case .prod: - services.switchAppEnvironment(to: .prod) - case .local: - services.switchAppEnvironment(to: .local) - } - } - } - .store(in: &subscriptions) - } -} - -public final class DebugMenuViewModel { - public enum Action { - case applyChanges(environment: DebugMenuEnvOption) - } - - let initialEnvironment: AppEnvironment - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init(initialEnvironment: AppEnvironment) { - self.initialEnvironment = initialEnvironment - } - - var initialDebugMenuEnvOption: DebugMenuEnvOption { - DebugMenuEnvOption.make(from: initialEnvironment) - } -} - -public enum DebugMenuEnvOption: String, CaseIterable { - case dev = "Dev" - case prod = "Prod" - case demo = "Demo" - case local = "Local" - - static func make(from appEnvironment: AppEnvironment) -> DebugMenuEnvOption { - switch appEnvironment { - case .local: - return .local - case .dev: - return .dev - case .prod: - return .prod - case .demo: - return .demo - case .test: - return .local - } - } -} - -public struct DebugMenuView: View { - @State private var selectedEnvironment: DebugMenuEnvOption - - private var viewModel: DebugMenuViewModel - - public init(viewModel: DebugMenuViewModel) { - self.viewModel = viewModel - self._selectedEnvironment = State(initialValue: viewModel.initialDebugMenuEnvOption) - } - - public var body: some View { - VStack { - Text("Debug Menu") - .font(.appTitle) - Form { - Text("API Environment:") - Picker(selection: $selectedEnvironment, label: Text("API Environment:")) { - ForEach(DebugMenuEnvOption.allCases, id: \.self) { - Text($0.rawValue) - } - } - .pickerStyle(SegmentedPickerStyle()) - } - - Button( - action: { viewModel.performActionSubject.send(.applyChanges(environment: selectedEnvironment)) }, - label: { Text("Apply Changes") } - ) - .buttonStyle(SolidCapsuleButtonStyle(width: 220)) - } - .padding() - } -} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift new file mode 100644 index 000000000..78cbb2308 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift @@ -0,0 +1,38 @@ +import Models +import SwiftUI +import Views + +struct DebugMenuView: View { + @State private var selectedEnvironment: AppEnvironment + + let appEnvironments: [AppEnvironment] = [.local, .demo, .dev, .prod] + let services: Services + + init(services: Services) { + self._selectedEnvironment = State(initialValue: services.dataService.appEnvironment) + self.services = services + } + + var body: some View { + VStack { + Text("Debug Menu") + .font(.appTitle) + Form { + Text("API Environment:") + Picker(selection: $selectedEnvironment, label: Text("API Environment:")) { + ForEach(appEnvironments, id: \.self) { + Text($0.rawValue) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + + Button( + action: { services.switchAppEnvironment(to: selectedEnvironment) }, + label: { Text("Apply Changes") } + ) + .buttonStyle(SolidCapsuleButtonStyle(width: 220)) + } + .padding() + } +} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift index 9d80f2283..4b7d701b1 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift @@ -76,7 +76,7 @@ struct WelcomeView: View { public var body: some View { primaryContent() .sheet(isPresented: $showDebugModal) { - DebugMenuView(viewModel: DebugMenuViewModel.make(services: services)) + DebugMenuView(services: services) } .onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 } } diff --git a/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift deleted file mode 100644 index 8b1378917..000000000 --- a/apple/OmnivoreKit/Sources/Views/DebugMenuView.swift +++ /dev/null @@ -1 +0,0 @@ - From 6837d08fd4e59796ebc038d74c14d2dd27e52a8b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:28:43 -0800 Subject: [PATCH 05/21] move RegistrationView into binder package --- .../Binders/Scenes/RegistrationScene.swift | 72 +++++++++++++++++++ .../OmnivoreKit/Sources/Models/AuthFlow.swift | 6 ++ .../Views/Buttons/AppleSignInButton.swift | 8 ++- .../Views/Buttons/GoogleAuthButton.swift | 8 ++- .../OmnivoreKit/Sources/Views/LocalText.swift | 4 +- .../LoginErrorMessageView.swift | 8 ++- .../RegistrationViews/RegistrationView.swift | 59 --------------- .../RegistrationViewModel.swift | 25 ------- .../ToggleAuthFlowButton.swift | 1 + 9 files changed, 99 insertions(+), 92 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Models/AuthFlow.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationView.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift index 3ff2640c0..9139fc21c 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift @@ -1,10 +1,27 @@ import AuthenticationServices +import Combine import Models import Services import SwiftUI import Utils import Views +public final class RegistrationViewModel: ObservableObject { + @Published public var loginError: LoginError? + @Published public var createProfileViewModel: CreateProfileViewModel? + @Published public var newAppleSignupViewModel: NewAppleSignupViewModel? + + public enum Action { + case googleButtonTapped + case appleSignInCompleted(result: Result) + } + + public var subscriptions = Set() + public let performActionSubject = PassthroughSubject() + + public init() {} +} + extension RegistrationViewModel { static func make(services: Services) -> RegistrationViewModel { let viewModel = RegistrationViewModel() @@ -114,3 +131,58 @@ private func presentingViewController() -> PlatformViewController? { return nil #endif } + +public struct RegistrationView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @ObservedObject private var viewModel: RegistrationViewModel + + public init(viewModel: RegistrationViewModel) { + self.viewModel = viewModel + } + + var authenticationView: some View { + VStack(spacing: 0) { + VStack(spacing: 28) { + if horizontalSizeClass == .regular { + Spacer() + } + + VStack(alignment: .center, spacing: 16) { + Text(LocalText.registrationViewHeadline) + .font(.appTitle) + .multilineTextAlignment(.center) + .padding(.bottom, horizontalSizeClass == .compact ? 0 : 50) + .padding(.top, horizontalSizeClass == .compact ? 30 : 0) + + AppleSignInButton { + viewModel.performActionSubject.send(.appleSignInCompleted(result: $0)) + } + + if AppKeys.sharedInstance?.iosClientGoogleId != nil { + GoogleAuthButton { + viewModel.performActionSubject.send(.googleButtonTapped) + } + } + } + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + + Spacer() + } + .frame(maxWidth: 316) + .padding(.horizontal, 16) + } + } + + public var body: some View { + if let createProfileViewModel = viewModel.createProfileViewModel { + CreateProfileView(viewModel: createProfileViewModel) + } else if let newAppleSignupViewModel = viewModel.newAppleSignupViewModel { + NewAppleSignupView(viewModel: newAppleSignupViewModel) + } else { + authenticationView + } + } +} diff --git a/apple/OmnivoreKit/Sources/Models/AuthFlow.swift b/apple/OmnivoreKit/Sources/Models/AuthFlow.swift new file mode 100644 index 000000000..78dfc11ff --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/AuthFlow.swift @@ -0,0 +1,6 @@ +import Foundation + +public enum AuthFlow { + case signIn + case signUp +} diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift b/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift index 2383a6251..b8381201f 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift @@ -1,12 +1,16 @@ import AuthenticationServices import SwiftUI -struct AppleSignInButton: View { +public struct AppleSignInButton: View { @Environment(\.colorScheme) var colorScheme let onCompletion: (Result) -> Void - var body: some View { + public init(onCompletion: @escaping (Result) -> Void) { + self.onCompletion = onCompletion + } + + public var body: some View { SignInWithAppleButton( .continue, onRequest: { request in diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/GoogleAuthButton.swift b/apple/OmnivoreKit/Sources/Views/Buttons/GoogleAuthButton.swift index d76d69df4..9296252c7 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/GoogleAuthButton.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/GoogleAuthButton.swift @@ -1,9 +1,13 @@ import SwiftUI -struct GoogleAuthButton: View { +public struct GoogleAuthButton: View { let tapAction: () -> Void - var body: some View { + public init(tapAction: @escaping () -> Void) { + self.tapAction = tapAction + } + + public var body: some View { Button(action: tapAction) { HStack(spacing: 8) { Image.googleIcon diff --git a/apple/OmnivoreKit/Sources/Views/LocalText.swift b/apple/OmnivoreKit/Sources/Views/LocalText.swift index 8bf363319..3e7b9d015 100644 --- a/apple/OmnivoreKit/Sources/Views/LocalText.swift +++ b/apple/OmnivoreKit/Sources/Views/LocalText.swift @@ -1,6 +1,6 @@ import Foundation -struct LocalText { +public enum LocalText { static func localText(key: String, comment: String? = nil) -> String { NSLocalizedString(key, bundle: .module, comment: comment ?? "no comment provided by developer") } @@ -9,7 +9,7 @@ struct LocalText { static let googleAuthButton = localText(key: "googleAuthButton") static let registrationViewSignInHeadline = localText(key: "registrationViewSignInHeadline") static let registrationViewSignUpHeadline = localText(key: "registrationViewSignUpHeadline") - static let registrationViewHeadline = localText(key: "registrationViewHeadline") + public static let registrationViewHeadline = localText(key: "registrationViewHeadline") static let networkError = localText(key: "error.network") static let genericError = localText(key: "error.generic") static let invalidCredsLoginError = localText(key: "loginError.invalidCreds") diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift index a475ee0f9..4e3ccf36c 100644 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift @@ -1,10 +1,14 @@ import Models import SwiftUI -struct LoginErrorMessageView: View { +public struct LoginErrorMessageView: View { let loginError: LoginError - var body: some View { + public init(loginError: LoginError) { + self.loginError = loginError + } + + public var body: some View { Text(loginError.message) .font(.appBody) .foregroundColor(.red) diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationView.swift deleted file mode 100644 index 528cb744c..000000000 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationView.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Combine -import Models -import SwiftUI -import Utils - -public struct RegistrationView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var viewModel: RegistrationViewModel - - public init(viewModel: RegistrationViewModel) { - self.viewModel = viewModel - } - - var authenticationView: some View { - VStack(spacing: 0) { - VStack(spacing: 28) { - if horizontalSizeClass == .regular { - Spacer() - } - - VStack(alignment: .center, spacing: 16) { - Text(LocalText.registrationViewHeadline) - .font(.appTitle) - .multilineTextAlignment(.center) - .padding(.bottom, horizontalSizeClass == .compact ? 0 : 50) - .padding(.top, horizontalSizeClass == .compact ? 30 : 0) - - AppleSignInButton { - viewModel.performActionSubject.send(.appleSignInCompleted(result: $0)) - } - - if AppKeys.sharedInstance?.iosClientGoogleId != nil { - GoogleAuthButton { - viewModel.performActionSubject.send(.googleButtonTapped) - } - } - } - - if let loginError = viewModel.loginError { - LoginErrorMessageView(loginError: loginError) - } - - Spacer() - } - .frame(maxWidth: 316) - .padding(.horizontal, 16) - } - } - - public var body: some View { - if let createProfileViewModel = viewModel.createProfileViewModel { - CreateProfileView(viewModel: createProfileViewModel) - } else if let newAppleSignupViewModel = viewModel.newAppleSignupViewModel { - NewAppleSignupView(viewModel: newAppleSignupViewModel) - } else { - authenticationView - } - } -} diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift deleted file mode 100644 index 9fc277e2a..000000000 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/RegistrationViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -import AuthenticationServices -import Combine -import Models -import SwiftUI - -enum AuthFlow { - case signIn - case signUp -} - -public final class RegistrationViewModel: ObservableObject { - @Published public var loginError: LoginError? - @Published public var createProfileViewModel: CreateProfileViewModel? - @Published public var newAppleSignupViewModel: NewAppleSignupViewModel? - - public enum Action { - case googleButtonTapped - case appleSignInCompleted(result: Result) - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init() {} -} diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/ToggleAuthFlowButton.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/ToggleAuthFlowButton.swift index a62c9feb8..f7c417fd3 100644 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/ToggleAuthFlowButton.swift +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/ToggleAuthFlowButton.swift @@ -1,3 +1,4 @@ +import Models import SwiftUI struct ToggleAuthFlowButton: View { From 0a8db1e4773b40f8846ba0ccc36a63f3932873ba Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:39:03 -0800 Subject: [PATCH 06/21] remove extra public calls in registration view model --- .../Binders/Scenes/RegistrationScene.swift | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift index 9139fc21c..f33a07db4 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift @@ -6,20 +6,21 @@ import SwiftUI import Utils import Views -public final class RegistrationViewModel: ObservableObject { - @Published public var loginError: LoginError? - @Published public var createProfileViewModel: CreateProfileViewModel? - @Published public var newAppleSignupViewModel: NewAppleSignupViewModel? +// TODO: remove this view model +final class RegistrationViewModel: ObservableObject { + @Published var loginError: LoginError? + @Published var createProfileViewModel: CreateProfileViewModel? + @Published var newAppleSignupViewModel: NewAppleSignupViewModel? - public enum Action { + enum Action { case googleButtonTapped case appleSignInCompleted(result: Result) } - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() + var subscriptions = Set() + let performActionSubject = PassthroughSubject() - public init() {} + init() {} } extension RegistrationViewModel { @@ -132,11 +133,11 @@ private func presentingViewController() -> PlatformViewController? { #endif } -public struct RegistrationView: View { +struct RegistrationView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: RegistrationViewModel - public init(viewModel: RegistrationViewModel) { + init(viewModel: RegistrationViewModel) { self.viewModel = viewModel } @@ -176,7 +177,7 @@ public struct RegistrationView: View { } } - public var body: some View { + var body: some View { if let createProfileViewModel = viewModel.createProfileViewModel { CreateProfileView(viewModel: createProfileViewModel) } else if let newAppleSignupViewModel = viewModel.newAppleSignupViewModel { From 731590c5e2bbefd84cbe5e972599ac1c5d43386d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:41:29 -0800 Subject: [PATCH 07/21] file rename --- .../RegistrationScene.swift => Views/RegistrationView.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apple/OmnivoreKit/Sources/Binders/{Scenes/RegistrationScene.swift => Views/RegistrationView.swift} (100%) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift b/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Binders/Scenes/RegistrationScene.swift rename to apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift From 78a24587703898d6dc8518b691c3612325664592 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:44:07 -0800 Subject: [PATCH 08/21] move NewAppleSignupView into binders package --- .../Binders/Scenes/NewAppleSignupScene.swift | 41 ------- .../Binders/Scenes/NewAppleSignupView.swift | 103 ++++++++++++++++++ .../NewAppleSignupView.swift | 64 ----------- 3 files changed, 103 insertions(+), 105 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupScene.swift create mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/RegistrationViews/NewAppleSignupView.swift diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupScene.swift deleted file mode 100644 index 3d26d4198..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupScene.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -extension NewAppleSignupViewModel { - static func make( - services: Services, - userProfile: UserProfile, - showProfileEditView: @escaping () -> Void - ) -> NewAppleSignupViewModel { - let viewModel = NewAppleSignupViewModel(userProfile: userProfile) - viewModel.bind(services: services, showProfileEditView: showProfileEditView) - return viewModel - } - - func bind(services: Services, showProfileEditView: @escaping () -> Void) { - performActionSubject.sink { [weak self] action in - switch action { - case let .acceptProfile(userProfile: userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case .changeProfile: - showProfileEditView() - } - } - .store(in: &subscriptions) - } - - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { - authenticator - .createAccount(userProfile: userProfile).sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(loginError) = completion else { return } - self?.loginError = loginError - }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) - } -} diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift new file mode 100644 index 000000000..8a86e11c4 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift @@ -0,0 +1,103 @@ +import Combine +import Models +import Services +import SwiftUI +import Utils +import Views + +extension NewAppleSignupViewModel { + static func make( + services: Services, + userProfile: UserProfile, + showProfileEditView: @escaping () -> Void + ) -> NewAppleSignupViewModel { + let viewModel = NewAppleSignupViewModel(userProfile: userProfile) + viewModel.bind(services: services, showProfileEditView: showProfileEditView) + return viewModel + } + + func bind(services: Services, showProfileEditView: @escaping () -> Void) { + performActionSubject.sink { [weak self] action in + switch action { + case let .acceptProfile(userProfile: userProfile): + self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) + case .changeProfile: + showProfileEditView() + } + } + .store(in: &subscriptions) + } + + private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + authenticator + .createAccount(userProfile: userProfile).sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(loginError) = completion else { return } + self?.loginError = loginError + }, + receiveValue: { _ in } + ) + .store(in: &subscriptions) + } +} + +final class NewAppleSignupViewModel: ObservableObject { + let userProfile: UserProfile + @Published var loginError: LoginError? + + enum Action { + case acceptProfile(userProfile: UserProfile) + case changeProfile + } + + var subscriptions = Set() + let performActionSubject = PassthroughSubject() + + init(userProfile: UserProfile) { + self.userProfile = userProfile + } +} + +struct NewAppleSignupView: View { + @ObservedObject private var viewModel: NewAppleSignupViewModel + + init(viewModel: NewAppleSignupViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(spacing: 28) { + Text("Welcome to Omnivore!") + .font(.appTitle) + .multilineTextAlignment(.center) + + VStack(alignment: .center, spacing: 12) { + Text("Your username is:") + .font(.appBody) + .foregroundColor(.appGrayText) + Text("@\(viewModel.userProfile.username)") + .font(.appHeadline) + .foregroundColor(.appGrayText) + } + + VStack { + Button( + action: { viewModel.performActionSubject.send(.acceptProfile(userProfile: viewModel.userProfile)) }, + label: { Text("Continue") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + Button( + action: { viewModel.performActionSubject.send(.changeProfile) }, + label: { Text("Change Username") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + } + } + .frame(maxWidth: 300) + } +} diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/NewAppleSignupView.swift deleted file mode 100644 index b05eb7030..000000000 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/NewAppleSignupView.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Combine -import Models -import SwiftUI - -public final class NewAppleSignupViewModel: ObservableObject { - let userProfile: UserProfile - @Published public var loginError: LoginError? - - public enum Action { - case acceptProfile(userProfile: UserProfile) - case changeProfile - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init(userProfile: UserProfile) { - self.userProfile = userProfile - } -} - -public struct NewAppleSignupView: View { - @ObservedObject private var viewModel: NewAppleSignupViewModel - - public init(viewModel: NewAppleSignupViewModel) { - self.viewModel = viewModel - } - - public var body: some View { - VStack(spacing: 28) { - Text("Welcome to Omnivore!") - .font(.appTitle) - .multilineTextAlignment(.center) - - VStack(alignment: .center, spacing: 12) { - Text("Your username is:") - .font(.appBody) - .foregroundColor(.appGrayText) - Text("@\(viewModel.userProfile.username)") - .font(.appHeadline) - .foregroundColor(.appGrayText) - } - - VStack { - Button( - action: { viewModel.performActionSubject.send(.acceptProfile(userProfile: viewModel.userProfile)) }, - label: { Text("Continue") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - - Button( - action: { viewModel.performActionSubject.send(.changeProfile) }, - label: { Text("Change Username") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - - if let loginError = viewModel.loginError { - LoginErrorMessageView(loginError: loginError) - } - } - } - .frame(maxWidth: 300) - } -} From 3289c5bcbbf2801ae3a6122d0efe5166a1f7ba16 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 15:50:21 -0800 Subject: [PATCH 09/21] move CreateProfileView into Binders package --- .../Binders/Scenes/CreateProfileScene.swift | 68 --------------- .../Views}/CreateProfileView.swift | 87 ++++++++++++++++--- .../NewAppleSignupView.swift | 1 + .../Views/TextFields/TextFieldStyles.swift | 7 +- 4 files changed, 82 insertions(+), 81 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/CreateProfileScene.swift rename apple/OmnivoreKit/Sources/{Views/ProfileViews => Binders/Views}/CreateProfileView.swift (67%) rename apple/OmnivoreKit/Sources/Binders/{Scenes => Views}/NewAppleSignupView.swift (98%) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/CreateProfileScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/CreateProfileScene.swift deleted file mode 100644 index 9b4f33981..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/CreateProfileScene.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -extension CreateProfileViewModel { - static func make(services: Services, pendingUserProfile: UserProfile) -> CreateProfileViewModel { - let viewModel = CreateProfileViewModel(initialUserProfile: pendingUserProfile) - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case let .submitProfile(userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case let .validateUsername(username: username): - self?.validateUsername(username: username, dataService: services.dataService) - } - } - .store(in: &subscriptions) - } - - private func validateUsername(username: String, dataService: DataService) { - if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) { - potentialUsernameStatus = status - return - } - - dataService.validateUsernamePublisher(username: username).sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(usernameError) = completion else { return } - switch usernameError { - case .tooShort: - self?.potentialUsernameStatus = .tooShort - case .tooLong: - self?.potentialUsernameStatus = .tooLong - case .invalidPattern: - self?.potentialUsernameStatus = .invalidPattern - case .nameUnavailable: - self?.potentialUsernameStatus = .unavailable - case .internalServer, .unknown: - self?.loginError = .unknown - case .network: - self?.loginError = .network - } - }, - receiveValue: { [weak self] in - self?.potentialUsernameStatus = .available - } - ) - .store(in: &subscriptions) - } - - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { - authenticator - .createAccount(userProfile: userProfile).sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(loginError) = completion else { return } - self?.loginError = loginError - }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) - } -} diff --git a/apple/OmnivoreKit/Sources/Views/ProfileViews/CreateProfileView.swift b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift similarity index 67% rename from apple/OmnivoreKit/Sources/Views/ProfileViews/CreateProfileView.swift rename to apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift index 8fb6e0e32..45d85e632 100644 --- a/apple/OmnivoreKit/Sources/Views/ProfileViews/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift @@ -1,8 +1,75 @@ import Combine import Models +import Services import SwiftUI +import Utils +import Views -public final class CreateProfileViewModel: ObservableObject { +extension CreateProfileViewModel { + static func make(services: Services, pendingUserProfile: UserProfile) -> CreateProfileViewModel { + let viewModel = CreateProfileViewModel(initialUserProfile: pendingUserProfile) + viewModel.bind(services: services) + return viewModel + } + + func bind(services: Services) { + performActionSubject.sink { [weak self] action in + switch action { + case let .submitProfile(userProfile): + self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) + case let .validateUsername(username: username): + self?.validateUsername(username: username, dataService: services.dataService) + } + } + .store(in: &subscriptions) + } + + private func validateUsername(username: String, dataService: DataService) { + if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) { + potentialUsernameStatus = status + return + } + + dataService.validateUsernamePublisher(username: username).sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(usernameError) = completion else { return } + switch usernameError { + case .tooShort: + self?.potentialUsernameStatus = .tooShort + case .tooLong: + self?.potentialUsernameStatus = .tooLong + case .invalidPattern: + self?.potentialUsernameStatus = .invalidPattern + case .nameUnavailable: + self?.potentialUsernameStatus = .unavailable + case .internalServer, .unknown: + self?.loginError = .unknown + case .network: + self?.loginError = .network + } + }, + receiveValue: { [weak self] in + self?.potentialUsernameStatus = .available + } + ) + .store(in: &subscriptions) + } + + private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + authenticator + .createAccount(userProfile: userProfile).sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(loginError) = completion else { return } + self?.loginError = loginError + }, + receiveValue: { _ in } + ) + .store(in: &subscriptions) + } +} + +// TODO: remove this view model +final class CreateProfileViewModel: ObservableObject { let initialUserProfile: UserProfile var hasSuggestedProfile: Bool { @@ -17,20 +84,20 @@ public final class CreateProfileViewModel: ObservableObject { hasSuggestedProfile ? "Confirm" : "Submit" } - @Published public var loginError: LoginError? + @Published var loginError: LoginError? @Published var validationErrorMessage: String? - @Published public var potentialUsernameStatus = PotentialUsernameStatus.noUsername + @Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername @Published var potentialUsername: String - public enum Action { + enum Action { case submitProfile(userProfile: UserProfile) case validateUsername(username: String) } - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() + var subscriptions = Set() + let performActionSubject = PassthroughSubject() - public init(initialUserProfile: UserProfile) { + init(initialUserProfile: UserProfile) { self.initialUserProfile = initialUserProfile self.potentialUsername = initialUserProfile.username @@ -58,14 +125,14 @@ public final class CreateProfileViewModel: ObservableObject { } } -public struct CreateProfileView: View { +struct CreateProfileView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: CreateProfileViewModel @State private var name: String @State private var bio = "" - public init(viewModel: CreateProfileViewModel) { + init(viewModel: CreateProfileViewModel) { self.viewModel = viewModel self._name = State(initialValue: viewModel.initialUserProfile.name) } @@ -74,7 +141,7 @@ public struct CreateProfileView: View { viewModel.submitProfile(name: name, bio: bio) } - public var body: some View { + var body: some View { VStack(spacing: 0) { VStack(spacing: 28) { ScrollView(showsIndicators: false) { diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift similarity index 98% rename from apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift rename to apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift index 8a86e11c4..2f1046da4 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/NewAppleSignupView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift @@ -41,6 +41,7 @@ extension NewAppleSignupViewModel { } } +// TODO: remove this view model final class NewAppleSignupViewModel: ObservableObject { let userProfile: UserProfile @Published var loginError: LoginError? diff --git a/apple/OmnivoreKit/Sources/Views/TextFields/TextFieldStyles.swift b/apple/OmnivoreKit/Sources/Views/TextFields/TextFieldStyles.swift index 49be3cb30..6f4a54c78 100644 --- a/apple/OmnivoreKit/Sources/Views/TextFields/TextFieldStyles.swift +++ b/apple/OmnivoreKit/Sources/Views/TextFields/TextFieldStyles.swift @@ -1,8 +1,9 @@ import SwiftUI -struct StandardTextFieldStyle: TextFieldStyle { +public struct StandardTextFieldStyle: TextFieldStyle { + public init() {} // swiftlint:disable:next identifier_name - func _body(configuration: TextField<_Label>) -> some View { + public func _body(configuration: TextField<_Label>) -> some View { configuration .textFieldStyle(PlainTextFieldStyle()) .multilineTextAlignment(.leading) @@ -13,7 +14,7 @@ struct StandardTextFieldStyle: TextFieldStyle { .background(border) } - var border: some View { + public var border: some View { RoundedRectangle(cornerRadius: 16) .strokeBorder(Color.appGrayBorder, lineWidth: 1) .background(RoundedRectangle(cornerRadius: 16).fill(Color.systemBackground)) From 39a50d20b41eb79d401aff95d1dfd2030a503f5b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:03:59 -0800 Subject: [PATCH 10/21] move home feed view into binders --- .../PrimaryContentCategory.swift | 13 +- .../Binders/Scenes/HomeFeedScene.swift | 197 --------------- .../Scenes}/HomeFeedView.swift | 225 ++++++++++++++++-- .../Sources/Views/Images/Images.swift | 2 +- .../HomeFeedCardView.swift | 8 +- .../Sources/Views/SnoozeView.swift | 22 +- 6 files changed, 240 insertions(+), 227 deletions(-) rename apple/OmnivoreKit/Sources/{Views/PrimaryContainerViews => Binders}/PrimaryContentCategory.swift (72%) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift rename apple/OmnivoreKit/Sources/{Views/PrimaryContainerViews => Binders/Scenes}/HomeFeedView.swift (55%) diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift similarity index 72% rename from apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift rename to apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift index 13919d14e..3b133c876 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift @@ -1,14 +1,15 @@ import SwiftUI +import Views -public enum PrimaryContentCategory: Identifiable, Hashable, Equatable { +enum PrimaryContentCategory: Identifiable, Hashable, Equatable { case feed(viewModel: HomeFeedViewModel) case profile(viewModel: ProfileContainerViewModel) - public static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { + static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { lhs.id == rhs.id } - public var id: String { + var id: String { title } @@ -39,11 +40,11 @@ public enum PrimaryContentCategory: Identifiable, Hashable, Equatable { } } - public var listLabel: some View { + var listLabel: some View { Label { Text(title) } icon: { image.renderingMode(.template) } } - @ViewBuilder public var destinationView: some View { + @ViewBuilder var destinationView: some View { switch self { case let .feed(viewModel: viewModel): HomeFeedView(viewModel: viewModel) @@ -52,7 +53,7 @@ public enum PrimaryContentCategory: Identifiable, Hashable, Equatable { } } - public func hash(into hasher: inout Hasher) { + func hash(into hasher: inout Hasher) { hasher.combine(id) } } diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift deleted file mode 100644 index 5f4541645..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift +++ /dev/null @@ -1,197 +0,0 @@ -import Models -import Services -import SwiftUI -import UserNotifications -import Utils -import Views - -extension HomeFeedViewModel { - static func make(services: Services) -> HomeFeedViewModel { - let viewModel = HomeFeedViewModel { feedItem in - LinkItemDetailViewModel.make(feedItem: feedItem, services: services) - } - - if UIDevice.isIPhone { - viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services) - } - - viewModel.bind(services: services) - viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case let .refreshItems(query: query): - self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: true) - case let .loadItems(query): - self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: false) - case let .archive(linkId): - self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: true) - case let .unarchive(linkId): - self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: false) - case let .remove(linkId): - self?.removeLink(dataService: services.dataService, linkId: linkId) - case let .snooze(linkId, until, successMessage): - self?.snoozeUntil( - dataService: services.dataService, - linkId: linkId, - until: until, - successMessage: successMessage - ) - } - } - .store(in: &subscriptions) - } - - private func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { - // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set - dataService.clearHighlights() - - let thisSearchIdx = searchIdx - searchIdx += 1 - - isLoading = true - startNetworkActivityIndicator() - - // Cache the viewer - if dataService.currentViewer == nil { - dataService.viewerPublisher().sink( - receiveCompletion: { _ in }, - receiveValue: { _ in } - ) - .store(in: &subscriptions) - } - - dataService.libraryItemsPublisher( - limit: 10, - sortDescending: true, - searchQuery: searchQuery, - cursor: isRefresh ? nil : cursor - ) - .sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.isLoading = false - stopNetworkActivityIndicator() - print(error) - }, - receiveValue: { [weak self] result in - // Search results aren't guaranteed to return in order so this - // will discard old results that are returned while a user is typing. - // For example if a user types 'Canucks', often the search results - // for 'C' are returned after 'Canucks' because it takes the backend - // much longer to compute. - if thisSearchIdx > 0, thisSearchIdx <= self?.receivedIdx ?? 0 { - return - } - self?.items = isRefresh ? result.items : (self?.items ?? []) + result.items - self?.isLoading = false - self?.receivedIdx = thisSearchIdx - self?.cursor = result.cursor - stopNetworkActivityIndicator() - } - ) - .store(in: &subscriptions) - } - - private func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) { - isLoading = true - startNetworkActivityIndicator() - - // First remove the link from the internal list, - // then make a call to remove it. The isLoading block should - // prevent our local change from being overwritten, but we - // might need to cache a local list of archived links - if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { - items.remove(at: itemIndex) - } - - dataService.archiveLinkPublisher(itemID: linkId, archived: archived) - .sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.isLoading = false - stopNetworkActivityIndicator() - print(error) - NSNotification.operationFailed(message: archived ? "Failed to archive link" : "Failed to unarchive link") - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - stopNetworkActivityIndicator() - NSNotification.operationSuccess(message: archived ? "Link archived" : "Link moved to Inbox") - } - ) - .store(in: &subscriptions) - } - - private func removeLink(dataService: DataService, linkId: String) { - isLoading = true - startNetworkActivityIndicator() - - if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { - items.remove(at: itemIndex) - } - - dataService.removeLinkPublisher(itemID: linkId) - .sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.isLoading = false - stopNetworkActivityIndicator() - print(error) - NSNotification.operationFailed(message: "Failed to remove link") - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - stopNetworkActivityIndicator() - NSNotification.operationSuccess(message: "Link removed") - } - ) - .store(in: &subscriptions) - } - - private func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) { - isLoading = true - startNetworkActivityIndicator() - - if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { - items.remove(at: itemIndex) - } - - dataService.createReminderPublisher( - reminderItemId: .link(id: linkId), - remindAt: until - ) - .sink( - receiveCompletion: { [weak self] completion in - guard case let .failure(error) = completion else { return } - self?.isLoading = false - stopNetworkActivityIndicator() - print(error) - NSNotification.operationFailed(message: "Failed to snooze") - }, - receiveValue: { [weak self] _ in - self?.isLoading = false - stopNetworkActivityIndicator() - if let message = successMessage { - NSNotification.operationSuccess(message: message) - } - } - ) - .store(in: &subscriptions) - } -} - -private func startNetworkActivityIndicator() { - #if os(iOS) - UIApplication.shared.isNetworkActivityIndicatorVisible = true - #endif -} - -private func stopNetworkActivityIndicator() { - #if os(iOS) - UIApplication.shared.isNetworkActivityIndicatorVisible = false - #endif -} diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedView.swift similarity index 55% rename from apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift rename to apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedView.swift index cea24590e..d93a78a13 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedView.swift @@ -1,24 +1,219 @@ import Combine import Models +import Services import SwiftUI +import UserNotifications import Utils +import Views -public final class HomeFeedViewModel: ObservableObject { +extension HomeFeedViewModel { + static func make(services: Services) -> HomeFeedViewModel { + let viewModel = HomeFeedViewModel { feedItem in + LinkItemDetailViewModel.make(feedItem: feedItem, services: services) + } + + if UIDevice.isIPhone { + viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services) + } + + viewModel.bind(services: services) + viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false) + return viewModel + } + + func bind(services: Services) { + performActionSubject.sink { [weak self] action in + switch action { + case let .refreshItems(query: query): + self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: true) + case let .loadItems(query): + self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: false) + case let .archive(linkId): + self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: true) + case let .unarchive(linkId): + self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: false) + case let .remove(linkId): + self?.removeLink(dataService: services.dataService, linkId: linkId) + case let .snooze(linkId, until, successMessage): + self?.snoozeUntil( + dataService: services.dataService, + linkId: linkId, + until: until, + successMessage: successMessage + ) + } + } + .store(in: &subscriptions) + } + + private func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { + // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set + dataService.clearHighlights() + + let thisSearchIdx = searchIdx + searchIdx += 1 + + isLoading = true + startNetworkActivityIndicator() + + // Cache the viewer + if dataService.currentViewer == nil { + dataService.viewerPublisher().sink( + receiveCompletion: { _ in }, + receiveValue: { _ in } + ) + .store(in: &subscriptions) + } + + dataService.libraryItemsPublisher( + limit: 10, + sortDescending: true, + searchQuery: searchQuery, + cursor: isRefresh ? nil : cursor + ) + .sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(error) = completion else { return } + self?.isLoading = false + stopNetworkActivityIndicator() + print(error) + }, + receiveValue: { [weak self] result in + // Search results aren't guaranteed to return in order so this + // will discard old results that are returned while a user is typing. + // For example if a user types 'Canucks', often the search results + // for 'C' are returned after 'Canucks' because it takes the backend + // much longer to compute. + if thisSearchIdx > 0, thisSearchIdx <= self?.receivedIdx ?? 0 { + return + } + self?.items = isRefresh ? result.items : (self?.items ?? []) + result.items + self?.isLoading = false + self?.receivedIdx = thisSearchIdx + self?.cursor = result.cursor + stopNetworkActivityIndicator() + } + ) + .store(in: &subscriptions) + } + + private func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) { + isLoading = true + startNetworkActivityIndicator() + + // First remove the link from the internal list, + // then make a call to remove it. The isLoading block should + // prevent our local change from being overwritten, but we + // might need to cache a local list of archived links + if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { + items.remove(at: itemIndex) + } + + dataService.archiveLinkPublisher(itemID: linkId, archived: archived) + .sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(error) = completion else { return } + self?.isLoading = false + stopNetworkActivityIndicator() + print(error) + NSNotification.operationFailed(message: archived ? "Failed to archive link" : "Failed to unarchive link") + }, + receiveValue: { [weak self] _ in + self?.isLoading = false + stopNetworkActivityIndicator() + NSNotification.operationSuccess(message: archived ? "Link archived" : "Link moved to Inbox") + } + ) + .store(in: &subscriptions) + } + + private func removeLink(dataService: DataService, linkId: String) { + isLoading = true + startNetworkActivityIndicator() + + if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { + items.remove(at: itemIndex) + } + + dataService.removeLinkPublisher(itemID: linkId) + .sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(error) = completion else { return } + self?.isLoading = false + stopNetworkActivityIndicator() + print(error) + NSNotification.operationFailed(message: "Failed to remove link") + }, + receiveValue: { [weak self] _ in + self?.isLoading = false + stopNetworkActivityIndicator() + NSNotification.operationSuccess(message: "Link removed") + } + ) + .store(in: &subscriptions) + } + + private func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) { + isLoading = true + startNetworkActivityIndicator() + + if let itemIndex = items.firstIndex(where: { $0.id == linkId }) { + items.remove(at: itemIndex) + } + + dataService.createReminderPublisher( + reminderItemId: .link(id: linkId), + remindAt: until + ) + .sink( + receiveCompletion: { [weak self] completion in + guard case let .failure(error) = completion else { return } + self?.isLoading = false + stopNetworkActivityIndicator() + print(error) + NSNotification.operationFailed(message: "Failed to snooze") + }, + receiveValue: { [weak self] _ in + self?.isLoading = false + stopNetworkActivityIndicator() + if let message = successMessage { + NSNotification.operationSuccess(message: message) + } + } + ) + .store(in: &subscriptions) + } +} + +private func startNetworkActivityIndicator() { + #if os(iOS) + UIApplication.shared.isNetworkActivityIndicatorVisible = true + #endif +} + +private func stopNetworkActivityIndicator() { + #if os(iOS) + UIApplication.shared.isNetworkActivityIndicatorVisible = false + #endif +} + +// TODO: remove this view model +final class HomeFeedViewModel: ObservableObject { let detailViewModelCreator: (FeedItem) -> LinkItemDetailViewModel var currentDetailViewModel: LinkItemDetailViewModel? - public var profileContainerViewModel: ProfileContainerViewModel? + var profileContainerViewModel: ProfileContainerViewModel? - @Published public var items = [FeedItem]() - @Published public var isLoading = false - @Published public var showPushNotificationPrimer = false - public var cursor: String? + @Published var items = [FeedItem]() + @Published var isLoading = false + @Published var showPushNotificationPrimer = false + var cursor: String? // These are used to make sure we handle search result // responses in the right order - public var searchIdx = 0 - public var receivedIdx = 0 + var searchIdx = 0 + var receivedIdx = 0 - public enum Action { + enum Action { case refreshItems(query: String) case loadItems(query: String) case archive(linkId: String) @@ -27,10 +222,10 @@ public final class HomeFeedViewModel: ObservableObject { case snooze(linkId: String, until: Date, successMessage: String?) } - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() + var subscriptions = Set() + let performActionSubject = PassthroughSubject() - public init(detailViewModelCreator: @escaping (FeedItem) -> LinkItemDetailViewModel) { + init(detailViewModelCreator: @escaping (FeedItem) -> LinkItemDetailViewModel) { self.detailViewModelCreator = detailViewModelCreator } @@ -50,7 +245,7 @@ public final class HomeFeedViewModel: ObservableObject { } } -public struct HomeFeedView: View { +struct HomeFeedView: View { @ObservedObject private var viewModel: HomeFeedViewModel @State private var selectedLinkItem: FeedItem? @State private var searchQuery = "" @@ -59,7 +254,7 @@ public struct HomeFeedView: View { @State private var snoozePresented = false @State private var itemToSnooze: FeedItem? - public init(viewModel: HomeFeedViewModel) { + init(viewModel: HomeFeedViewModel) { self.viewModel = viewModel } @@ -255,7 +450,7 @@ public struct HomeFeedView: View { } } - public var body: some View { + var body: some View { #if os(iOS) if UIDevice.isIPhone, let profileContainerViewModel = viewModel.profileContainerViewModel { NavigationView { diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.swift b/apple/OmnivoreKit/Sources/Views/Images/Images.swift index 9f9526a5a..e612a4afa 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.swift +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.swift @@ -1,6 +1,6 @@ import SwiftUI -extension Image { +public extension Image { static var smallOmnivoreLogo: Image { Image("_smallOmnivoreLogo", bundle: .module) } static var omnivoreTitleLogo: Image { Image("_omnivoreTitleLogo", bundle: .module) } static var readingIllustration: Image { Image("_readingIllustration", bundle: .module) } diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift index ff7858469..310759e6c 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift @@ -1,10 +1,14 @@ import Models import SwiftUI -struct FeedCard: View { +public struct FeedCard: View { let item: FeedItem - var body: some View { + public init(item: FeedItem) { + self.item = item + } + + public var body: some View { HStack(alignment: .top, spacing: 6) { VStack(alignment: .leading, spacing: 6) { Text(item.title) diff --git a/apple/OmnivoreKit/Sources/Views/SnoozeView.swift b/apple/OmnivoreKit/Sources/Views/SnoozeView.swift index 7901412cf..8c23a926e 100644 --- a/apple/OmnivoreKit/Sources/Views/SnoozeView.swift +++ b/apple/OmnivoreKit/Sources/Views/SnoozeView.swift @@ -1,12 +1,22 @@ import Models import SwiftUI -struct SnoozeView: View { +public struct SnoozeView: View { @Binding var snoozePresented: Bool @Binding var itemToSnooze: FeedItem? let snoozeAction: (SnoozeActionParams) -> Void - var body: some View { + public init( + snoozePresented: Binding, + itemToSnooze: Binding, + snoozeAction: @escaping (SnoozeActionParams) -> Void + ) { + self._snoozePresented = snoozePresented + self._itemToSnooze = itemToSnooze + self.snoozeAction = snoozeAction + } + + public var body: some View { VStack { Spacer() @@ -42,10 +52,10 @@ struct SnoozeView: View { } } -struct SnoozeActionParams { - let feedItemId: String - let snoozeUntilDate: Date - let successMessage: String? +public struct SnoozeActionParams { + public let feedItemId: String + public let snoozeUntilDate: Date + public let successMessage: String? } private struct SnoozeIconButtonView: View { From b6e73d34047ed325b64ed122ed527f5ff9e9bd3c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:04:29 -0800 Subject: [PATCH 11/21] move home feed view into views folder --- .../Sources/Binders/{Scenes => Views}/HomeFeedView.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apple/OmnivoreKit/Sources/Binders/{Scenes => Views}/HomeFeedView.swift (100%) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedView.swift rename to apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift From 48de634e984c8f427e7a0f4b41471196a2cae271 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:13:29 -0800 Subject: [PATCH 12/21] move ProfileContainerViewModel into Binders package --- .../Scenes/ProfileContainerScene.swift | 115 ++++++++++++++++++ .../Sources/Binders/Views/HomeFeedView.swift | 8 +- .../Profile/ProfileCard.swift | 8 +- .../Profile/ProfileContainerView.swift | 114 ----------------- .../UserSettings/ManageAccountView.swift | 8 +- .../Sources/Views/Utils/MacOSSizeClass.swift | 12 +- .../Sources/Views/Web/BasicWebAppView.swift | 26 ++-- 7 files changed, 155 insertions(+), 136 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift index 86a7dba1d..049e47e59 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift @@ -1,7 +1,11 @@ +import Combine +import Models import Services import SwiftUI +import Utils import Views +// TODO: remove this view model extension ProfileContainerViewModel { static func make(services: Services) -> ProfileContainerViewModel { let viewModel = ProfileContainerViewModel() @@ -39,3 +43,114 @@ extension ProfileContainerViewModel { .store(in: &subscriptions) } } + +final class ProfileContainerViewModel: ObservableObject { + @Published var isLoading = false + @Published var profileCardData = ProfileCardData() + + enum Action { + case logout + case loadProfileData + case showIntercomMessenger + case deleteAccount + } + + var subscriptions = Set() + let performActionSubject = PassthroughSubject() + + init() {} +} + +struct ProfileContainerView: View { + @ObservedObject private var viewModel: ProfileContainerViewModel + @State private var showLogoutConfirmation = false + + init(viewModel: ProfileContainerViewModel) { + self.viewModel = viewModel + } + + var body: some View { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + + private var innerBody: some View { + Group { + Section { + ProfileCard(data: viewModel.profileCardData) + .onAppear { + viewModel.performActionSubject.send(.loadProfileData) + } + } + + Section { + NavigationLink(destination: BasicWebAppView.privacyPolicyWebView) { + Text("Privacy Policy") + } + + NavigationLink(destination: BasicWebAppView.termsConditionsWebView) { + Text("Terms and Conditions") + } + + #if os(iOS) + Button( + action: { + viewModel.performActionSubject.send(.showIntercomMessenger) + }, + label: { Text("Feedback") } + ) + #endif + } + + Section { + if FeatureFlag.showAccountDeletion { + NavigationLink( + destination: ManageAccountView(handleAccountDeletion: { + viewModel.performActionSubject.send(.deleteAccount) + }) + ) { + Text("Manage Account") + } + } + + Text("Logout") + .onTapGesture { + showLogoutConfirmation = true + } + .alert(isPresented: $showLogoutConfirmation) { + Alert( + title: Text("Are you sure you want to logout?"), + primaryButton: .destructive(Text("Confirm")) { + viewModel.performActionSubject.send(.logout) + }, + secondaryButton: .cancel() + ) + } + } + } + .navigationTitle("Profile") + } +} + +private extension BasicWebAppView { + static let privacyPolicyWebView: BasicWebAppView = { + omnivoreWebView(path: "privacy") + }() + + static let termsConditionsWebView: BasicWebAppView = { + omnivoreWebView(path: "terms") + }() + + private static func omnivoreWebView(path: String) -> BasicWebAppView { + let urlString = "https://omnivore.app/\(path)?isAppEmbedView=true" + return BasicWebAppView(request: URLRequest(url: URL(string: urlString)!)) + } +} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift index d93a78a13..5b0706d57 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift @@ -12,9 +12,11 @@ extension HomeFeedViewModel { LinkItemDetailViewModel.make(feedItem: feedItem, services: services) } - if UIDevice.isIPhone { - viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services) - } + #if os(iOS) + if UIDevice.isIPhone { + viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services) + } + #endif viewModel.bind(services: services) viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false) diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift index b2d415805..a27ad3857 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift @@ -12,10 +12,14 @@ public struct ProfileCardData { } } -struct ProfileCard: View { +public struct ProfileCard: View { let data: ProfileCardData - var body: some View { + public init(data: ProfileCardData) { + self.data = data + } + + public var body: some View { HStack(alignment: .center) { Group { if let url = data.imageURL { diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift index 3c3a9fc16..8b1378917 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift @@ -1,115 +1 @@ -import Combine -import Models -import SwiftUI -import Utils -public final class ProfileContainerViewModel: ObservableObject { - @Published public var isLoading = false - @Published public var profileCardData = ProfileCardData() - - public enum Action { - case logout - case loadProfileData - case showIntercomMessenger - case deleteAccount - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init() {} -} - -public struct ProfileContainerView: View { - @ObservedObject private var viewModel: ProfileContainerViewModel - @State private var showLogoutConfirmation = false - - public init(viewModel: ProfileContainerViewModel) { - self.viewModel = viewModel - } - - public var body: some View { - #if os(iOS) - Form { - innerBody - } - #elseif os(macOS) - List { - innerBody - } - .listStyle(InsetListStyle()) - #endif - } - - private var innerBody: some View { - Group { - Section { - ProfileCard(data: viewModel.profileCardData) - .onAppear { - viewModel.performActionSubject.send(.loadProfileData) - } - } - - Section { - NavigationLink(destination: BasicWebAppView.privacyPolicyWebView) { - Text("Privacy Policy") - } - - NavigationLink(destination: BasicWebAppView.termsConditionsWebView) { - Text("Terms and Conditions") - } - - #if os(iOS) - Button( - action: { - viewModel.performActionSubject.send(.showIntercomMessenger) - }, - label: { Text("Feedback") } - ) - #endif - } - - Section { - if FeatureFlag.showAccountDeletion { - NavigationLink( - destination: ManageAccountView(handleAccountDeletion: { - viewModel.performActionSubject.send(.deleteAccount) - }) - ) { - Text("Manage Account") - } - } - - Text("Logout") - .onTapGesture { - showLogoutConfirmation = true - } - .alert(isPresented: $showLogoutConfirmation) { - Alert( - title: Text("Are you sure you want to logout?"), - primaryButton: .destructive(Text("Confirm")) { - viewModel.performActionSubject.send(.logout) - }, - secondaryButton: .cancel() - ) - } - } - } - .navigationTitle("Profile") - } -} - -private extension BasicWebAppView { - static let privacyPolicyWebView: BasicWebAppView = { - omnivoreWebView(path: "privacy") - }() - - static let termsConditionsWebView: BasicWebAppView = { - omnivoreWebView(path: "terms") - }() - - private static func omnivoreWebView(path: String) -> BasicWebAppView { - let urlString = "https://omnivore.app/\(path)?isAppEmbedView=true" - return BasicWebAppView(request: URLRequest(url: URL(string: urlString)!)) - } -} diff --git a/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift b/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift index 247860487..1b6d589ca 100644 --- a/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift +++ b/apple/OmnivoreKit/Sources/Views/UserSettings/ManageAccountView.swift @@ -1,10 +1,14 @@ import SwiftUI -struct ManageAccountView: View { +public struct ManageAccountView: View { let handleAccountDeletion: () -> Void @State private var showDeleteAccountConfirmation = false - var body: some View { + public init(handleAccountDeletion: @escaping () -> Void) { + self.handleAccountDeletion = handleAccountDeletion + } + + public var body: some View { Button( action: { showDeleteAccountConfirmation = true diff --git a/apple/OmnivoreKit/Sources/Views/Utils/MacOSSizeClass.swift b/apple/OmnivoreKit/Sources/Views/Utils/MacOSSizeClass.swift index 85c3adff9..f53d08f91 100644 --- a/apple/OmnivoreKit/Sources/Views/Utils/MacOSSizeClass.swift +++ b/apple/OmnivoreKit/Sources/Views/Utils/MacOSSizeClass.swift @@ -8,20 +8,20 @@ import SwiftUI // https://stackoverflow.com/questions/63526478/swiftui-userinterfacesizeclass-for-universal-macos-ios-views #if os(macOS) - enum UserInterfaceSizeClass { + public enum UserInterfaceSizeClass { case compact case regular } - struct HorizontalSizeClassEnvironmentKey: EnvironmentKey { - static let defaultValue: UserInterfaceSizeClass = .regular + public struct HorizontalSizeClassEnvironmentKey: EnvironmentKey { + public static let defaultValue: UserInterfaceSizeClass = .regular } - struct VerticalSizeClassEnvironmentKey: EnvironmentKey { - static let defaultValue: UserInterfaceSizeClass = .regular + public struct VerticalSizeClassEnvironmentKey: EnvironmentKey { + public static let defaultValue: UserInterfaceSizeClass = .regular } - extension EnvironmentValues { + public extension EnvironmentValues { var horizontalSizeClass: UserInterfaceSizeClass { get { self[HorizontalSizeClassEnvironmentKey.self] } set { self[HorizontalSizeClassEnvironmentKey.self] = newValue } diff --git a/apple/OmnivoreKit/Sources/Views/Web/BasicWebAppView.swift b/apple/OmnivoreKit/Sources/Views/Web/BasicWebAppView.swift index ae4231c4a..370760116 100644 --- a/apple/OmnivoreKit/Sources/Views/Web/BasicWebAppView.swift +++ b/apple/OmnivoreKit/Sources/Views/Web/BasicWebAppView.swift @@ -2,22 +2,26 @@ import SwiftUI import WebKit #if os(iOS) - struct BasicWebAppView: UIViewRepresentable { + public struct BasicWebAppView: UIViewRepresentable { let request: URLRequest let webView = WKWebView() - func makeCoordinator() -> BasicWebAppViewCoordinator { + public init(request: URLRequest) { + self.request = request + } + + public func makeCoordinator() -> BasicWebAppViewCoordinator { BasicWebAppViewCoordinator() } - func makeUIView(context _: Context) -> WKWebView { + public func makeUIView(context _: Context) -> WKWebView { webView.scrollView.isScrollEnabled = true webView.isOpaque = false webView.backgroundColor = UIColor.clear return webView } - func updateUIView(_ webView: WKWebView, context: Context) { + public func updateUIView(_ webView: WKWebView, context: Context) { if context.coordinator.needsReload { webView.load(request) context.coordinator.needsReload = false @@ -27,18 +31,22 @@ import WebKit #endif #if os(macOS) - struct BasicWebAppView: NSViewRepresentable { + public struct BasicWebAppView: NSViewRepresentable { let request: URLRequest - func makeCoordinator() -> BasicWebAppViewCoordinator { + public init(request: URLRequest) { + self.request = request + } + + public func makeCoordinator() -> BasicWebAppViewCoordinator { BasicWebAppViewCoordinator() } - func makeNSView(context _: Context) -> WKWebView { + public func makeNSView(context _: Context) -> WKWebView { WebView(frame: CGRect.zero) } - func updateNSView(_ webView: WKWebView, context: Context) { + public func updateNSView(_ webView: WKWebView, context: Context) { if context.coordinator.needsReload { webView.load(request) context.coordinator.needsReload = false @@ -47,7 +55,7 @@ import WebKit } #endif -final class BasicWebAppViewCoordinator: NSObject { +public final class BasicWebAppViewCoordinator: NSObject { var needsReload = true override init() { From 2bbe0e22d1382bd477a7ab668b51bff2fb38928f Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:14:58 -0800 Subject: [PATCH 13/21] move files around --- .../ProfileContainerView.swift} | 0 .../Views/{PrimaryContainerViews => }/HomeFeedCardView.swift | 0 .../PrimaryContainerViews/Profile/ProfileContainerView.swift | 1 - .../Views/{PrimaryContainerViews/Profile => }/ProfileCard.swift | 0 4 files changed, 1 deletion(-) rename apple/OmnivoreKit/Sources/Binders/{Scenes/ProfileContainerScene.swift => Views/ProfileContainerView.swift} (100%) rename apple/OmnivoreKit/Sources/Views/{PrimaryContainerViews => }/HomeFeedCardView.swift (100%) delete mode 100644 apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift rename apple/OmnivoreKit/Sources/Views/{PrimaryContainerViews/Profile => }/ProfileCard.swift (100%) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift b/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Binders/Scenes/ProfileContainerScene.swift rename to apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/HomeFeedCardView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedCardView.swift rename to apple/OmnivoreKit/Sources/Views/HomeFeedCardView.swift diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift deleted file mode 100644 index 8b1378917..000000000 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift b/apple/OmnivoreKit/Sources/Views/ProfileCard.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileCard.swift rename to apple/OmnivoreKit/Sources/Views/ProfileCard.swift From 5e4502719e53695a01623857ec5e65d455a2ea30 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:24:21 -0800 Subject: [PATCH 14/21] move LinkItemDetailViewModel into binders package --- .../Binders/Scenes/LinkItemDetailScene.swift | 86 -------------- .../Views}/LinkItemDetailView.swift | 108 ++++++++++++++++-- .../Sources/Views/Article/WebAppView.swift | 4 +- .../Views/Article/WebAppViewCoordinator.swift | 2 +- .../Views/Article/WebAppWrapperView.swift | 4 +- .../Views/FontSizeAdjustmentPopoverView.swift | 8 ++ apple/OmnivoreKit/Sources/Views/Popover.swift | 2 +- 7 files changed, 110 insertions(+), 104 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Binders/Scenes/LinkItemDetailScene.swift rename apple/OmnivoreKit/Sources/{Views/LinkedItemDetail => Binders/Views}/LinkItemDetailView.swift (61%) diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/LinkItemDetailScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/LinkItemDetailScene.swift deleted file mode 100644 index 3955b7e3f..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/LinkItemDetailScene.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -extension LinkItemDetailViewModel { - static func make(feedItem: FeedItem, services: Services) -> LinkItemDetailViewModel { - let viewModel = LinkItemDetailViewModel(item: feedItem) - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case .load: - self?.loadWebAppWrapper(services: services) - case let .updateReadStatus(markAsRead: markAsRead): - self?.updateItemReadStatus(markAsRead: markAsRead, dataService: services.dataService) - } - } - .store(in: &subscriptions) - } - - private func updateItemReadStatus(markAsRead: Bool, dataService: DataService) { - dataService - .updateArticleReadingProgressPublisher( - itemID: item.id, - readingProgress: markAsRead ? 100 : 0, - anchorIndex: 0 - ) - .sink { completion in - guard case let .failure(error) = completion else { return } - print(error) - } receiveValue: { [weak self] feedItem in - self?.item.readingProgress = feedItem.readingProgress - } - .store(in: &subscriptions) - } - - private func loadWebAppWrapper(services: Services) { - // Attempt to get `Viewer` from DataService - if let currentViewer = services.dataService.currentViewer { - createWebAppWrapperViewModel(username: currentViewer.username, services: services) - return - } - - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - self?.createWebAppWrapperViewModel(username: viewer.username, services: services) - } - ) - .store(in: &subscriptions) - } - - private func createWebAppWrapperViewModel(username: String, services: Services) { - let baseURL = services.dataService.appEnvironment.webAppBaseURL - - let urlRequest = URLRequest.webRequest( - baseURL: services.dataService.appEnvironment.webAppBaseURL, - urlPath: "/app/\(username)/\(item.slug)", - queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"] - ) - - let newWebAppWrapperViewModel = WebAppWrapperViewModel( - webViewURLRequest: urlRequest, - baseURL: baseURL, - rawAuthCookie: services.authenticator.omnivoreAuthCookieString - ) - - newWebAppWrapperViewModel.performActionSubject.sink { action in - switch action { - case let .shareHighlight(highlightID): - print("show share modal for highlight with id: \(highlightID)") - } - } - .store(in: &newWebAppWrapperViewModel.subscriptions) - - webAppWrapperViewModel = newWebAppWrapperViewModel - } -} diff --git a/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift similarity index 61% rename from apple/OmnivoreKit/Sources/Views/LinkedItemDetail/LinkItemDetailView.swift rename to apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift index 3250a37e8..ac491a6cb 100644 --- a/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift @@ -1,30 +1,114 @@ import Combine import Models +import Services import SwiftUI import Utils +import Views -public enum PDFProvider { - public static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)? +// TODO: remove this view model +extension LinkItemDetailViewModel { + static func make(feedItem: FeedItem, services: Services) -> LinkItemDetailViewModel { + let viewModel = LinkItemDetailViewModel(item: feedItem) + viewModel.bind(services: services) + return viewModel + } + + func bind(services: Services) { + performActionSubject.sink { [weak self] action in + switch action { + case .load: + self?.loadWebAppWrapper(services: services) + case let .updateReadStatus(markAsRead: markAsRead): + self?.updateItemReadStatus(markAsRead: markAsRead, dataService: services.dataService) + } + } + .store(in: &subscriptions) + } + + private func updateItemReadStatus(markAsRead: Bool, dataService: DataService) { + dataService + .updateArticleReadingProgressPublisher( + itemID: item.id, + readingProgress: markAsRead ? 100 : 0, + anchorIndex: 0 + ) + .sink { completion in + guard case let .failure(error) = completion else { return } + print(error) + } receiveValue: { [weak self] feedItem in + self?.item.readingProgress = feedItem.readingProgress + } + .store(in: &subscriptions) + } + + private func loadWebAppWrapper(services: Services) { + // Attempt to get `Viewer` from DataService + if let currentViewer = services.dataService.currentViewer { + createWebAppWrapperViewModel(username: currentViewer.username, services: services) + return + } + + services.dataService.viewerPublisher().sink( + receiveCompletion: { completion in + guard case let .failure(error) = completion else { return } + print(error) + }, + receiveValue: { [weak self] viewer in + self?.createWebAppWrapperViewModel(username: viewer.username, services: services) + } + ) + .store(in: &subscriptions) + } + + private func createWebAppWrapperViewModel(username: String, services: Services) { + let baseURL = services.dataService.appEnvironment.webAppBaseURL + + let urlRequest = URLRequest.webRequest( + baseURL: services.dataService.appEnvironment.webAppBaseURL, + urlPath: "/app/\(username)/\(item.slug)", + queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"] + ) + + let newWebAppWrapperViewModel = WebAppWrapperViewModel( + webViewURLRequest: urlRequest, + baseURL: baseURL, + rawAuthCookie: services.authenticator.omnivoreAuthCookieString + ) + + newWebAppWrapperViewModel.performActionSubject.sink { action in + switch action { + case let .shareHighlight(highlightID): + print("show share modal for highlight with id: \(highlightID)") + } + } + .store(in: &newWebAppWrapperViewModel.subscriptions) + + webAppWrapperViewModel = newWebAppWrapperViewModel + } } -public final class LinkItemDetailViewModel: ObservableObject { - @Published public var item: FeedItem - @Published public var webAppWrapperViewModel: WebAppWrapperViewModel? +enum PDFProvider { + static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)? +} - public enum Action { +final class LinkItemDetailViewModel: ObservableObject { + @Published var item: FeedItem + @Published var webAppWrapperViewModel: WebAppWrapperViewModel? + + enum Action { case load case updateReadStatus(markAsRead: Bool) } - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() + var subscriptions = Set() + let performActionSubject = PassthroughSubject() - public init(item: FeedItem) { + init(item: FeedItem) { self.item = item } } -public struct LinkItemDetailView: View { +struct LinkItemDetailView: View { @Environment(\.presentationMode) var presentationMode: Binding static let navBarHeight = 50.0 @@ -32,7 +116,7 @@ public struct LinkItemDetailView: View { @State private var showFontSizePopover = false @State private var navBarVisibilityRatio = 1.0 - public init(viewModel: LinkItemDetailViewModel) { + init(viewModel: LinkItemDetailViewModel) { self.viewModel = viewModel } @@ -61,7 +145,7 @@ public struct LinkItemDetailView: View { ) } - public var body: some View { + var body: some View { #if os(iOS) if UIDevice.isIPhone, !viewModel.item.isPDF { compactInnerBody diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift index e725392f9..bc131ad93 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift @@ -34,8 +34,8 @@ import WebKit webView.backgroundColor = UIColor.clear webView.configuration.userContentController = contentController webView.scrollView.delegate = context.coordinator - webView.scrollView.contentInset.top = LinkItemDetailView.navBarHeight - webView.scrollView.verticalScrollIndicatorInsets.top = LinkItemDetailView.navBarHeight + webView.scrollView.contentInset.top = 50.0 // TODO: LinkItemDetailView.navBarHeight + webView.scrollView.verticalScrollIndicatorInsets.top = 50.0 // TODO: LinkItemDetailView.navBarHeight for action in WebViewAction.allCases { webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift index 1f54a5fdd..a14acfb53 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift @@ -2,7 +2,7 @@ import SwiftUI import WebKit final class WebAppViewCoordinator: NSObject { - let navBarHeight = LinkItemDetailView.navBarHeight + let navBarHeight = 50.0 // TODO: LinkItemDetailView.navBarHeight var webViewActionHandler: (WKScriptMessage) -> Void = { _ in } var linkHandler: (URL) -> Void = { _ in } var needsReload = true diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift index f87b67573..6f704e6b1 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppWrapperView.swift @@ -14,8 +14,8 @@ public final class WebAppWrapperViewModel: ObservableObject { let baseURL: URL let rawAuthCookie: String? - @Published var sendIncreaseFontSignal: Bool = false - @Published var sendDecreaseFontSignal: Bool = false + @Published public var sendIncreaseFontSignal: Bool = false + @Published public var sendDecreaseFontSignal: Bool = false public init(webViewURLRequest: URLRequest, baseURL: URL, rawAuthCookie: String?) { self.webViewURLRequest = webViewURLRequest diff --git a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift index deff8c4f8..76ec5520e 100644 --- a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift +++ b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift @@ -5,6 +5,14 @@ public struct FontSizeAdjustmentPopoverView: View { let increaseFontAction: () -> Void let decreaseFontAction: () -> Void + public init( + increaseFontAction: @escaping () -> Void, + decreaseFontAction: @escaping () -> Void + ) { + self.increaseFontAction = increaseFontAction + self.decreaseFontAction = decreaseFontAction + } + static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue #if os(macOS) @AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) diff --git a/apple/OmnivoreKit/Sources/Views/Popover.swift b/apple/OmnivoreKit/Sources/Views/Popover.swift index 91d9dfd2f..5cba95786 100644 --- a/apple/OmnivoreKit/Sources/Views/Popover.swift +++ b/apple/OmnivoreKit/Sources/Views/Popover.swift @@ -1,7 +1,7 @@ import SwiftUI #if os(iOS) - extension View { + public extension View { func fittedPopover( isPresented: Binding, onDismiss: (() -> Void)? = nil, From b1655c06e43441f98fb482df52118e75cefa2d0a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 16:27:00 -0800 Subject: [PATCH 15/21] make pdfwrapper public --- .../Views/LinkedItemDetail/PDFWrapperView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/PDFWrapperView.swift b/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/PDFWrapperView.swift index 1cee80cb8..9ca637bc6 100644 --- a/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/PDFWrapperView.swift +++ b/apple/OmnivoreKit/Sources/Views/LinkedItemDetail/PDFWrapperView.swift @@ -2,10 +2,14 @@ import PDFKit import SwiftUI #if os(macOS) - struct PDFWrapperView: NSViewRepresentable { + public struct PDFWrapperView: NSViewRepresentable { let pdfURL: URL - func makeNSView(context _: Context) -> PDFView { + public init(pdfURL: URL) { + self.pdfURL = pdfURL + } + + public func makeNSView(context _: Context) -> PDFView { let pdfView = PDFView() if let document = PDFDocument(url: pdfURL) { pdfView.document = document @@ -13,7 +17,7 @@ import SwiftUI return pdfView } - func updateNSView(_: PDFView, context _: Context) {} + public func updateNSView(_: PDFView, context _: Context) {} } #endif From 7f9f04989c182798fe86854936aae8aedcac11b0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 17:08:16 -0800 Subject: [PATCH 16/21] set authenticator and dataservice on the environmentobject --- .../Sources/Binders/PrimaryContentCategory.swift | 10 +--------- .../Sources/Binders/RootViewModel.swift | 2 ++ apple/OmnivoreKit/Sources/Binders/Services.swift | 5 ----- .../Sources/Binders/Views/DebugMenuView.swift | 14 +++++++++----- .../Sources/Binders/Views/WelcomeView.swift | 3 ++- .../Sources/Services/DataService/DataService.swift | 2 +- 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift index 3b133c876..46d73436d 100644 --- a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift @@ -1,6 +1,7 @@ import SwiftUI import Views +// TODO: maybe this can be removed?? enum PrimaryContentCategory: Identifiable, Hashable, Equatable { case feed(viewModel: HomeFeedViewModel) case profile(viewModel: ProfileContainerViewModel) @@ -31,15 +32,6 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { } } - var selectedImage: Image { - switch self { - case .feed: - return .homeTabSelected - case .profile: - return .profileTabSelected - } - } - var listLabel: some View { Label { Text(title) } icon: { image.renderingMode(.template) } } diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index 9259d3f65..6957d492d 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -185,6 +185,8 @@ public struct RootView: View { .frame(minWidth: 400, idealWidth: 1200, minHeight: 400, idealHeight: 1200) #endif } + .environmentObject(viewModel.services.authenticator) + .environmentObject(viewModel.services.dataService) #if os(iOS) .onOpenURL { url in withoutAnimation { diff --git a/apple/OmnivoreKit/Sources/Binders/Services.swift b/apple/OmnivoreKit/Sources/Binders/Services.swift index 041dc7298..c73ff44bb 100644 --- a/apple/OmnivoreKit/Sources/Binders/Services.swift +++ b/apple/OmnivoreKit/Sources/Binders/Services.swift @@ -11,9 +11,4 @@ public final class Services { self.authenticator = Authenticator(networker: networker) self.dataService = DataService(appEnvironment: appEnvironment, networker: networker) } - - public func switchAppEnvironment(to appEnvironment: AppEnvironment) { - authenticator.logout() - dataService.switchAppEnvironment(appEnvironment: appEnvironment) - } } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift index 78cbb2308..33feeb86a 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/DebugMenuView.swift @@ -1,16 +1,17 @@ import Models +import Services import SwiftUI import Views struct DebugMenuView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService @State private var selectedEnvironment: AppEnvironment let appEnvironments: [AppEnvironment] = [.local, .demo, .dev, .prod] - let services: Services - init(services: Services) { - self._selectedEnvironment = State(initialValue: services.dataService.appEnvironment) - self.services = services + init(initialEnvironment: AppEnvironment) { + self._selectedEnvironment = State(initialValue: initialEnvironment) } var body: some View { @@ -28,7 +29,10 @@ struct DebugMenuView: View { } Button( - action: { services.switchAppEnvironment(to: selectedEnvironment) }, + action: { + authenticator.logout() + dataService.switchAppEnvironment(appEnvironment: selectedEnvironment) + }, label: { Text("Apply Changes") } ) .buttonStyle(SolidCapsuleButtonStyle(width: 220)) diff --git a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift index 4b7d701b1..f88e7983e 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift @@ -5,6 +5,7 @@ import Utils import Views struct WelcomeView: View { + @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass let services: Services @State private var showRegistrationView = false @@ -76,7 +77,7 @@ struct WelcomeView: View { public var body: some View { primaryContent() .sheet(isPresented: $showDebugModal) { - DebugMenuView(services: services) + DebugMenuView(initialEnvironment: dataService.appEnvironment) } .onReceive(Publishers.keyboardHeight) { isKeyboardOnScreen = $0 > 1 } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 4cf69272e..12deb1da3 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -1,7 +1,7 @@ import Foundation import Models -public final class DataService { +public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? From 201576ec7a85b71c2538ef2168c05725404ed6c0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 21:06:54 -0800 Subject: [PATCH 17/21] use environment object to access services in registration views --- .../Sources/Binders/RootViewModel.swift | 2 +- .../Binders/Views/CreateProfileView.swift | 129 +++++++--------- .../Binders/Views/NewAppleSignupView.swift | 60 ++------ .../Binders/Views/RegistrationView.swift | 139 +++++++----------- .../Sources/Binders/Views/WelcomeView.swift | 3 +- 5 files changed, 123 insertions(+), 210 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index 6957d492d..662d83dbe 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -170,7 +170,7 @@ public struct RootView: View { #endif } else { - WelcomeView(services: viewModel.services) + WelcomeView() .accessibilityElement() .accessibilityIdentifier("welcomeView") } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift index 45d85e632..06f608c10 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift @@ -5,26 +5,56 @@ import SwiftUI import Utils import Views -extension CreateProfileViewModel { - static func make(services: Services, pendingUserProfile: UserProfile) -> CreateProfileViewModel { - let viewModel = CreateProfileViewModel(initialUserProfile: pendingUserProfile) - viewModel.bind(services: services) - return viewModel +final class CreateProfileViewModel: ObservableObject { + let initialUserProfile: UserProfile + + var hasSuggestedProfile: Bool { + !(initialUserProfile.name.isEmpty && initialUserProfile.username.isEmpty) } - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case let .submitProfile(userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case let .validateUsername(username: username): - self?.validateUsername(username: username, dataService: services.dataService) - } + var headlineText: String { + hasSuggestedProfile ? "Confirm Your Profile" : "Create Your Profile" + } + + var submitButtonText: String { + hasSuggestedProfile ? "Confirm" : "Submit" + } + + @Published var loginError: LoginError? + @Published var validationErrorMessage: String? + @Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername + @Published var potentialUsername: String + + var subscriptions = Set() + + init(initialUserProfile: UserProfile, dataService: DataService) { + self.initialUserProfile = initialUserProfile + self.potentialUsername = initialUserProfile.username + + $potentialUsername + .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] username in + self?.validateUsername(username: username, dataService: dataService) + }) + .store(in: &subscriptions) + } + + func submitProfile(name: String, bio: String, authenticator: Authenticator) { + let profileOrError = UserProfile.make( + username: potentialUsername, + name: name, + bio: bio.isEmpty ? nil : bio + ) + + switch profileOrError { + case let .left(userProfile): + submitProfile(userProfile: userProfile, authenticator: authenticator) + case let .right(errorMessage): + validationErrorMessage = errorMessage } - .store(in: &subscriptions) } - private func validateUsername(username: String, dataService: DataService) { + func validateUsername(username: String, dataService: DataService) { if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) { potentialUsernameStatus = status return @@ -55,7 +85,7 @@ extension CreateProfileViewModel { .store(in: &subscriptions) } - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { authenticator .createAccount(userProfile: userProfile).sink( receiveCompletion: { [weak self] completion in @@ -68,77 +98,22 @@ extension CreateProfileViewModel { } } -// TODO: remove this view model -final class CreateProfileViewModel: ObservableObject { - let initialUserProfile: UserProfile - - var hasSuggestedProfile: Bool { - !(initialUserProfile.name.isEmpty && initialUserProfile.username.isEmpty) - } - - var headlineText: String { - hasSuggestedProfile ? "Confirm Your Profile" : "Create Your Profile" - } - - var submitButtonText: String { - hasSuggestedProfile ? "Confirm" : "Submit" - } - - @Published var loginError: LoginError? - @Published var validationErrorMessage: String? - @Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername - @Published var potentialUsername: String - - enum Action { - case submitProfile(userProfile: UserProfile) - case validateUsername(username: String) - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init(initialUserProfile: UserProfile) { - self.initialUserProfile = initialUserProfile - self.potentialUsername = initialUserProfile.username - - $potentialUsername - .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] username in - self?.performActionSubject.send(.validateUsername(username: username)) - }) - .store(in: &subscriptions) - } - - func submitProfile(name: String, bio: String) { - let profileOrError = UserProfile.make( - username: potentialUsername, - name: name, - bio: bio.isEmpty ? nil : bio - ) - - switch profileOrError { - case let .left(userProfile): - performActionSubject.send(.submitProfile(userProfile: userProfile)) - case let .right(errorMessage): - validationErrorMessage = errorMessage - } - } -} - struct CreateProfileView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: CreateProfileViewModel @State private var name: String @State private var bio = "" - init(viewModel: CreateProfileViewModel) { - self.viewModel = viewModel - self._name = State(initialValue: viewModel.initialUserProfile.name) + init(userProfile: UserProfile, dataService: DataService) { + self.viewModel = CreateProfileViewModel(initialUserProfile: userProfile, dataService: dataService) + self._name = State(initialValue: userProfile.name) } private func didTapSubmitButton() { - viewModel.submitProfile(name: name, bio: bio) + viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator) } var body: some View { diff --git a/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift index 2f1046da4..d433ede10 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift @@ -5,30 +5,17 @@ import SwiftUI import Utils import Views -extension NewAppleSignupViewModel { - static func make( - services: Services, - userProfile: UserProfile, - showProfileEditView: @escaping () -> Void - ) -> NewAppleSignupViewModel { - let viewModel = NewAppleSignupViewModel(userProfile: userProfile) - viewModel.bind(services: services, showProfileEditView: showProfileEditView) - return viewModel +final class NewAppleSignupViewModel: ObservableObject { + let userProfile: UserProfile + @Published var loginError: LoginError? + + var subscriptions = Set() + + init(userProfile: UserProfile) { + self.userProfile = userProfile } - func bind(services: Services, showProfileEditView: @escaping () -> Void) { - performActionSubject.sink { [weak self] action in - switch action { - case let .acceptProfile(userProfile: userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case .changeProfile: - showProfileEditView() - } - } - .store(in: &subscriptions) - } - - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + func submitProfile(authenticator: Authenticator) { authenticator .createAccount(userProfile: userProfile).sink( receiveCompletion: { [weak self] completion in @@ -41,29 +28,14 @@ extension NewAppleSignupViewModel { } } -// TODO: remove this view model -final class NewAppleSignupViewModel: ObservableObject { - let userProfile: UserProfile - @Published var loginError: LoginError? - - enum Action { - case acceptProfile(userProfile: UserProfile) - case changeProfile - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init(userProfile: UserProfile) { - self.userProfile = userProfile - } -} - struct NewAppleSignupView: View { + @EnvironmentObject var authenticator: Authenticator @ObservedObject private var viewModel: NewAppleSignupViewModel + let showProfileEditView: () -> Void - init(viewModel: NewAppleSignupViewModel) { - self.viewModel = viewModel + init(userProfile: UserProfile, showProfileEditView: @escaping () -> Void) { + self.showProfileEditView = showProfileEditView + self.viewModel = NewAppleSignupViewModel(userProfile: userProfile) } var body: some View { @@ -83,13 +55,13 @@ struct NewAppleSignupView: View { VStack { Button( - action: { viewModel.performActionSubject.send(.acceptProfile(userProfile: viewModel.userProfile)) }, + action: { viewModel.submitProfile(authenticator: authenticator) }, label: { Text("Continue") } ) .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) Button( - action: { viewModel.performActionSubject.send(.changeProfile) }, + action: showProfileEditView, label: { Text("Change Username") } ) .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) diff --git a/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift b/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift index f33a07db4..aabb45de8 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift @@ -6,61 +6,38 @@ import SwiftUI import Utils import Views -// TODO: remove this view model final class RegistrationViewModel: ObservableObject { - @Published var loginError: LoginError? - @Published var createProfileViewModel: CreateProfileViewModel? - @Published var newAppleSignupViewModel: NewAppleSignupViewModel? - - enum Action { - case googleButtonTapped - case appleSignInCompleted(result: Result) + enum RegistrationState { + case createProfile(userProfile: UserProfile) + case newAppleSignUp(userProfile: UserProfile) } + @Published var loginError: LoginError? + @Published var registrationState: RegistrationState? + var subscriptions = Set() - let performActionSubject = PassthroughSubject() - init() {} -} - -extension RegistrationViewModel { - static func make(services: Services) -> RegistrationViewModel { - let viewModel = RegistrationViewModel() - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - self?.loginError = nil - - switch action { - case .googleButtonTapped: - self?.handleGoogleAuth(services: services) - case let .appleSignInCompleted(result: result): - switch AppleSigninPayload.parse(authResult: result) { - case let .success(payload): - self?.handleAppleToken(payload: payload, services: services) - case let .failure(error): - switch error { - case .unauthorized, .unknown: - break - case .network: - self?.loginError = error - } - } + func handleAppleSignInCompletion(result: Result, authenticator: Authenticator) { + switch AppleSigninPayload.parse(authResult: result) { + case let .success(payload): + handleAppleToken(payload: payload, authenticator: authenticator) + case let .failure(error): + switch error { + case .unauthorized, .unknown: + break + case .network: + loginError = error } } - .store(in: &subscriptions) } - private func handleAppleToken(payload: AppleSigninPayload, services: Services) { - services.authenticator.submitAppleToken(token: payload.token).sink( + private func handleAppleToken(payload: AppleSigninPayload, authenticator: Authenticator) { + authenticator.submitAppleToken(token: payload.token).sink( receiveCompletion: { [weak self] completion in guard case let .failure(loginError) = completion else { return } switch loginError { case .unauthorized, .unknown: - self?.handleAppleSignUp(services: services, payload: payload) + self?.handleAppleSignUp(authenticator: authenticator, payload: payload) case .network: self?.loginError = loginError } @@ -70,8 +47,8 @@ extension RegistrationViewModel { .store(in: &subscriptions) } - private func handleAppleSignUp(services: Services, payload: AppleSigninPayload) { - services.authenticator + private func handleAppleSignUp(authenticator: Authenticator, payload: AppleSigninPayload) { + authenticator .createPendingAccountUsingApple(token: payload.token, name: payload.fullName) .sink( receiveCompletion: { [weak self] completion in @@ -80,23 +57,17 @@ extension RegistrationViewModel { }, receiveValue: { [weak self] userProfile in if userProfile.name.isEmpty { - self?.showProfileEditView(services: services, pendingUserProfile: userProfile) + self?.registrationState = .createProfile(userProfile: userProfile) } else { - self?.newAppleSignupViewModel = NewAppleSignupViewModel.make( - services: services, - userProfile: userProfile, - showProfileEditView: { - self?.showProfileEditView(services: services, pendingUserProfile: userProfile) - } - ) + self?.registrationState = .newAppleSignUp(userProfile: userProfile) } } ) .store(in: &subscriptions) } - private func handleGoogleAuth(services: Services) { - services.authenticator + func handleGoogleAuth(authenticator: Authenticator) { + authenticator .handleGoogleAuth(presentingViewController: presentingViewController()) .sink( receiveCompletion: { [weak self] completion in @@ -105,41 +76,19 @@ extension RegistrationViewModel { }, receiveValue: { [weak self] isNewAccount in if isNewAccount { - let pendingUserProfile = UserProfile(username: "", name: "") - self?.showProfileEditView(services: services, pendingUserProfile: pendingUserProfile) + self?.registrationState = .createProfile(userProfile: UserProfile(username: "", name: "")) } } ) .store(in: &subscriptions) } - - func showProfileEditView(services: Services, pendingUserProfile: UserProfile) { - createProfileViewModel = CreateProfileViewModel.make( - services: services, - pendingUserProfile: pendingUserProfile - ) - newAppleSignupViewModel = nil - } -} - -private func presentingViewController() -> PlatformViewController? { - #if os(iOS) - return UIApplication.shared.windows - .filter(\.isKeyWindow) - .first? - .rootViewController - #elseif os(macOS) - return nil - #endif } struct RegistrationView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var viewModel: RegistrationViewModel - - init(viewModel: RegistrationViewModel) { - self.viewModel = viewModel - } + @ObservedObject private var viewModel = RegistrationViewModel() var authenticationView: some View { VStack(spacing: 0) { @@ -156,12 +105,12 @@ struct RegistrationView: View { .padding(.top, horizontalSizeClass == .compact ? 30 : 0) AppleSignInButton { - viewModel.performActionSubject.send(.appleSignInCompleted(result: $0)) + viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator) } if AppKeys.sharedInstance?.iosClientGoogleId != nil { GoogleAuthButton { - viewModel.performActionSubject.send(.googleButtonTapped) + viewModel.handleGoogleAuth(authenticator: authenticator) } } } @@ -178,12 +127,30 @@ struct RegistrationView: View { } var body: some View { - if let createProfileViewModel = viewModel.createProfileViewModel { - CreateProfileView(viewModel: createProfileViewModel) - } else if let newAppleSignupViewModel = viewModel.newAppleSignupViewModel { - NewAppleSignupView(viewModel: newAppleSignupViewModel) + if let registrationState = viewModel.registrationState { + if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState { + CreateProfileView(userProfile: userProfile, dataService: dataService) + } else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState { + NewAppleSignupView( + userProfile: userProfile, + showProfileEditView: { viewModel.registrationState = .createProfile(userProfile: userProfile) } + ) + } else { + authenticationView + } } else { authenticationView } } } + +private func presentingViewController() -> PlatformViewController? { + #if os(iOS) + return UIApplication.shared.windows + .filter(\.isKeyWindow) + .first? + .rootViewController + #elseif os(macOS) + return nil + #endif +} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift index f88e7983e..757893380 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift @@ -7,7 +7,6 @@ import Views struct WelcomeView: View { @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass - let services: Services @State private var showRegistrationView = false @State private var isKeyboardOnScreen = false @State private var showDebugModal = false @@ -21,7 +20,7 @@ struct WelcomeView: View { @ViewBuilder func userInteractiveView(width: CGFloat) -> some View { Group { if showRegistrationView { - RegistrationView(viewModel: RegistrationViewModel.make(services: services)) + RegistrationView() } else { GetStartedView(showRegistrationView: $showRegistrationView) } From 6a41484bca4a775912e8492c231f5646c60bb895 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 21:24:05 -0800 Subject: [PATCH 18/21] set navBarHeight in one place --- .../Sources/Binders/Views/LinkItemDetailView.swift | 2 +- apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift | 6 ++++-- .../Sources/Views/Article/WebAppViewCoordinator.swift | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift index ac491a6cb..68fe931bb 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift @@ -180,7 +180,7 @@ struct LinkItemDetailView: View { .padding(.horizontal) .scaleEffect(navBarVisibilityRatio) } - .frame(height: LinkItemDetailView.navBarHeight * navBarVisibilityRatio) + .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) .opacity(navBarVisibilityRatio) .background(Color.systemBackground) } diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift index bc131ad93..2ab5680ef 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift @@ -3,6 +3,8 @@ import SwiftUI import Utils import WebKit +public let readerViewNavBarHeight = 50.0 + #if os(iOS) struct WebAppView: UIViewRepresentable { let request: URLRequest @@ -34,8 +36,8 @@ import WebKit webView.backgroundColor = UIColor.clear webView.configuration.userContentController = contentController webView.scrollView.delegate = context.coordinator - webView.scrollView.contentInset.top = 50.0 // TODO: LinkItemDetailView.navBarHeight - webView.scrollView.verticalScrollIndicatorInsets.top = 50.0 // TODO: LinkItemDetailView.navBarHeight + webView.scrollView.contentInset.top = readerViewNavBarHeight + webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight for action in WebViewAction.allCases { webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift index a14acfb53..96ab85343 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift @@ -2,7 +2,6 @@ import SwiftUI import WebKit final class WebAppViewCoordinator: NSObject { - let navBarHeight = 50.0 // TODO: LinkItemDetailView.navBarHeight var webViewActionHandler: (WKScriptMessage) -> Void = { _ in } var linkHandler: (URL) -> Void = { _ in } var needsReload = true From 732941a6206e6bc9cdff1953e810bb706e7f4ccd Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 22:20:07 -0800 Subject: [PATCH 19/21] remove link item creator and create view directly instead --- .../Sources/Binders/Views/HomeFeedView.swift | 14 +- .../Binders/Views/LinkItemDetailView.swift | 168 +++++++++--------- .../Views/Article/WebAppViewCoordinator.swift | 18 +- 3 files changed, 97 insertions(+), 103 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift index 5b0706d57..7cd4c6ee2 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift @@ -8,9 +8,7 @@ import Views extension HomeFeedViewModel { static func make(services: Services) -> HomeFeedViewModel { - let viewModel = HomeFeedViewModel { feedItem in - LinkItemDetailViewModel.make(feedItem: feedItem, services: services) - } + let viewModel = HomeFeedViewModel() #if os(iOS) if UIDevice.isIPhone { @@ -201,7 +199,6 @@ private func stopNetworkActivityIndicator() { // TODO: remove this view model final class HomeFeedViewModel: ObservableObject { - let detailViewModelCreator: (FeedItem) -> LinkItemDetailViewModel var currentDetailViewModel: LinkItemDetailViewModel? var profileContainerViewModel: ProfileContainerViewModel? @@ -227,9 +224,7 @@ final class HomeFeedViewModel: ObservableObject { var subscriptions = Set() let performActionSubject = PassthroughSubject() - init(detailViewModelCreator: @escaping (FeedItem) -> LinkItemDetailViewModel) { - self.detailViewModelCreator = detailViewModelCreator - } + init() {} func itemAppeared(item: FeedItem, searchQuery: String) { if isLoading { return } @@ -248,6 +243,9 @@ final class HomeFeedViewModel: ObservableObject { } struct HomeFeedView: View { +// @EnvironmentObject var authenticator: Authenticator +// @EnvironmentObject var dataService: DataService + @ObservedObject private var viewModel: HomeFeedViewModel @State private var selectedLinkItem: FeedItem? @State private var searchQuery = "" @@ -314,7 +312,7 @@ struct HomeFeedView: View { ForEach(viewModel.items) { item in let link = ZStack { NavigationLink( - destination: LinkItemDetailView(viewModel: viewModel.detailViewModelCreator(item)), + destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item)), tag: item, selection: $selectedLinkItem ) { diff --git a/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift index 68fe931bb..7753cd4b6 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/LinkItemDetailView.swift @@ -5,88 +5,6 @@ import SwiftUI import Utils import Views -// TODO: remove this view model -extension LinkItemDetailViewModel { - static func make(feedItem: FeedItem, services: Services) -> LinkItemDetailViewModel { - let viewModel = LinkItemDetailViewModel(item: feedItem) - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case .load: - self?.loadWebAppWrapper(services: services) - case let .updateReadStatus(markAsRead: markAsRead): - self?.updateItemReadStatus(markAsRead: markAsRead, dataService: services.dataService) - } - } - .store(in: &subscriptions) - } - - private func updateItemReadStatus(markAsRead: Bool, dataService: DataService) { - dataService - .updateArticleReadingProgressPublisher( - itemID: item.id, - readingProgress: markAsRead ? 100 : 0, - anchorIndex: 0 - ) - .sink { completion in - guard case let .failure(error) = completion else { return } - print(error) - } receiveValue: { [weak self] feedItem in - self?.item.readingProgress = feedItem.readingProgress - } - .store(in: &subscriptions) - } - - private func loadWebAppWrapper(services: Services) { - // Attempt to get `Viewer` from DataService - if let currentViewer = services.dataService.currentViewer { - createWebAppWrapperViewModel(username: currentViewer.username, services: services) - return - } - - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - self?.createWebAppWrapperViewModel(username: viewer.username, services: services) - } - ) - .store(in: &subscriptions) - } - - private func createWebAppWrapperViewModel(username: String, services: Services) { - let baseURL = services.dataService.appEnvironment.webAppBaseURL - - let urlRequest = URLRequest.webRequest( - baseURL: services.dataService.appEnvironment.webAppBaseURL, - urlPath: "/app/\(username)/\(item.slug)", - queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"] - ) - - let newWebAppWrapperViewModel = WebAppWrapperViewModel( - webViewURLRequest: urlRequest, - baseURL: baseURL, - rawAuthCookie: services.authenticator.omnivoreAuthCookieString - ) - - newWebAppWrapperViewModel.performActionSubject.sink { action in - switch action { - case let .shareHighlight(highlightID): - print("show share modal for highlight with id: \(highlightID)") - } - } - .store(in: &newWebAppWrapperViewModel.subscriptions) - - webAppWrapperViewModel = newWebAppWrapperViewModel - } -} - enum PDFProvider { static var pdfViewerProvider: ((URL, FeedItem) -> AnyView)? } @@ -101,14 +19,84 @@ final class LinkItemDetailViewModel: ObservableObject { } var subscriptions = Set() - let performActionSubject = PassthroughSubject() init(item: FeedItem) { self.item = item } + + func updateItemReadStatus(dataService: DataService) { + dataService + .updateArticleReadingProgressPublisher( + itemID: item.id, + readingProgress: item.isRead ? 0 : 100, + anchorIndex: 0 + ) + .sink { completion in + guard case let .failure(error) = completion else { return } + print(error) + } receiveValue: { [weak self] feedItem in + self?.item.readingProgress = feedItem.readingProgress + } + .store(in: &subscriptions) + } + + func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) { + // Attempt to get `Viewer` from DataService + if let currentViewer = dataService.currentViewer { + createWebAppWrapperViewModel( + username: currentViewer.username, + dataService: dataService, + rawAuthCookie: rawAuthCookie + ) + return + } + + dataService.viewerPublisher().sink( + receiveCompletion: { completion in + guard case let .failure(error) = completion else { return } + print(error) + }, + receiveValue: { [weak self] viewer in + self?.createWebAppWrapperViewModel( + username: viewer.username, + dataService: dataService, + rawAuthCookie: rawAuthCookie + ) + } + ) + .store(in: &subscriptions) + } + + private func createWebAppWrapperViewModel(username: String, dataService: DataService, rawAuthCookie: String?) { + let baseURL = dataService.appEnvironment.webAppBaseURL + + let urlRequest = URLRequest.webRequest( + baseURL: dataService.appEnvironment.webAppBaseURL, + urlPath: "/app/\(username)/\(item.slug)", + queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"] + ) + + let newWebAppWrapperViewModel = WebAppWrapperViewModel( + webViewURLRequest: urlRequest, + baseURL: baseURL, + rawAuthCookie: rawAuthCookie + ) + + newWebAppWrapperViewModel.performActionSubject.sink { action in + switch action { + case let .shareHighlight(highlightID): + print("show share modal for highlight with id: \(highlightID)") + } + } + .store(in: &newWebAppWrapperViewModel.subscriptions) + + webAppWrapperViewModel = newWebAppWrapperViewModel + } } struct LinkItemDetailView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService @Environment(\.presentationMode) var presentationMode: Binding static let navBarHeight = 50.0 @@ -122,7 +110,9 @@ struct LinkItemDetailView: View { var toggleReadStatusToolbarItem: some View { Button( - action: { viewModel.performActionSubject.send(.updateReadStatus(markAsRead: !viewModel.item.isRead)) }, + action: { + viewModel.updateItemReadStatus(dataService: dataService) + }, label: { Image(systemName: viewModel.item.isRead ? "line.horizontal.3.decrease.circle" : "checkmark.circle") } @@ -232,7 +222,10 @@ struct LinkItemDetailView: View { Spacer() } .onAppear { - viewModel.performActionSubject.send(.load) + viewModel.loadWebAppWrapper( + dataService: dataService, + rawAuthCookie: authenticator.omnivoreAuthCookieString + ) } .navigationBarHidden(true) } @@ -275,7 +268,10 @@ struct LinkItemDetailView: View { Spacer() } .onAppear { - viewModel.performActionSubject.send(.load) + viewModel.loadWebAppWrapper( + dataService: dataService, + rawAuthCookie: authenticator.omnivoreAuthCookieString + ) } } } diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift index 96ab85343..cfe31bc1a 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppViewCoordinator.swift @@ -57,21 +57,21 @@ extension WebAppViewCoordinator: WKNavigationDelegate { let yOffset = scrollView.contentOffset.y if yOffset == 0 { - scrollView.contentInset.top = navBarHeight + scrollView.contentInset.top = readerViewNavBarHeight navBarVisibilityRatio = 1 return } if yOffset < 0 { navBarVisibilityRatio = 1 - scrollView.contentInset.top = navBarHeight + scrollView.contentInset.top = readerViewNavBarHeight return } - if yOffset < navBarHeight { + if yOffset < readerViewNavBarHeight { let isScrollingUp = yOffsetAtStartOfDrag ?? 0 > yOffset - navBarVisibilityRatio = isScrollingUp || yOffset < 0 ? 1 : min(1, 1 - (yOffset / navBarHeight)) - scrollView.contentInset.top = navBarVisibilityRatio * navBarHeight + navBarVisibilityRatio = isScrollingUp || yOffset < 0 ? 1 : min(1, 1 - (yOffset / readerViewNavBarHeight)) + scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight return } @@ -79,21 +79,21 @@ extension WebAppViewCoordinator: WKNavigationDelegate { if yOffset > yOffsetAtStartOfDrag, !isNavBarHidden { let translation = yOffset - yOffsetAtStartOfDrag - let ratio = translation < navBarHeight ? 1 - (translation / navBarHeight) : 0 + let ratio = translation < readerViewNavBarHeight ? 1 - (translation / readerViewNavBarHeight) : 0 navBarVisibilityRatio = min(ratio, 1) - scrollView.contentInset.top = navBarVisibilityRatio * navBarHeight + scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight } } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if decelerate, scrollView.contentOffset.y + scrollView.contentInset.top < (yOffsetAtStartOfDrag ?? 0) { - scrollView.contentInset.top = navBarHeight + scrollView.contentInset.top = readerViewNavBarHeight navBarVisibilityRatio = 1 } } func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { - scrollView.contentInset.top = navBarHeight + scrollView.contentInset.top = readerViewNavBarHeight navBarVisibilityRatio = 1 return false } From bb63806f0839e6edf96978e80be368b938cfe5e7 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 22:49:58 -0800 Subject: [PATCH 20/21] use env object for ProfileContainerView --- .../Binders/PrimaryContentCategory.swift | 6 +- .../Sources/Binders/Views/HomeFeedView.swift | 38 +++++------- .../Binders/Views/PrimaryContentView.swift | 4 +- .../Binders/Views/ProfileContainerView.swift | 60 ++++--------------- 4 files changed, 30 insertions(+), 78 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift index 46d73436d..ea2e75b33 100644 --- a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift @@ -4,7 +4,7 @@ import Views // TODO: maybe this can be removed?? enum PrimaryContentCategory: Identifiable, Hashable, Equatable { case feed(viewModel: HomeFeedViewModel) - case profile(viewModel: ProfileContainerViewModel) + case profile static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { lhs.id == rhs.id @@ -40,8 +40,8 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { switch self { case let .feed(viewModel: viewModel): HomeFeedView(viewModel: viewModel) - case let .profile(viewModel: viewModel): - ProfileContainerView(viewModel: viewModel) + case .profile: + ProfileContainerView() } } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift index 7cd4c6ee2..83bedbee2 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift @@ -9,13 +9,6 @@ import Views extension HomeFeedViewModel { static func make(services: Services) -> HomeFeedViewModel { let viewModel = HomeFeedViewModel() - - #if os(iOS) - if UIDevice.isIPhone { - viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services) - } - #endif - viewModel.bind(services: services) viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false) return viewModel @@ -185,22 +178,9 @@ extension HomeFeedViewModel { } } -private func startNetworkActivityIndicator() { - #if os(iOS) - UIApplication.shared.isNetworkActivityIndicatorVisible = true - #endif -} - -private func stopNetworkActivityIndicator() { - #if os(iOS) - UIApplication.shared.isNetworkActivityIndicatorVisible = false - #endif -} - // TODO: remove this view model final class HomeFeedViewModel: ObservableObject { var currentDetailViewModel: LinkItemDetailViewModel? - var profileContainerViewModel: ProfileContainerViewModel? @Published var items = [FeedItem]() @Published var isLoading = false @@ -452,15 +432,13 @@ struct HomeFeedView: View { var body: some View { #if os(iOS) - if UIDevice.isIPhone, let profileContainerViewModel = viewModel.profileContainerViewModel { + if UIDevice.isIPhone { NavigationView { conditionalInnerBody .toolbar { ToolbarItem { NavigationLink( - destination: { - ProfileContainerView(viewModel: profileContainerViewModel) - }, + destination: { ProfileContainerView() }, label: { Image.profile .resizable() @@ -484,3 +462,15 @@ struct HomeFeedView: View { viewModel.performActionSubject.send(.refreshItems(query: searchQuery)) } } + +private func startNetworkActivityIndicator() { + #if os(iOS) + UIApplication.shared.isNetworkActivityIndicatorVisible = true + #endif +} + +private func stopNetworkActivityIndicator() { + #if os(iOS) + UIApplication.shared.isNetworkActivityIndicatorVisible = false + #endif +} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift index 3c6fa23be..e0aa59060 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift @@ -5,11 +5,9 @@ import Views public struct PrimaryContentView: View { let homeFeedViewModel: HomeFeedViewModel - let profileContainerViewModel: ProfileContainerViewModel public init(services: Services) { self.homeFeedViewModel = HomeFeedViewModel.make(services: services) - self.profileContainerViewModel = ProfileContainerViewModel.make(services: services) } public var body: some View { @@ -33,7 +31,7 @@ public struct PrimaryContentView: View { private var regularView: some View { let categories = [ PrimaryContentCategory.feed(viewModel: homeFeedViewModel), - PrimaryContentCategory.profile(viewModel: profileContainerViewModel) + PrimaryContentCategory.profile ] return NavigationView { diff --git a/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift index 049e47e59..907ba3418 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift @@ -5,31 +5,13 @@ import SwiftUI import Utils import Views -// TODO: remove this view model -extension ProfileContainerViewModel { - static func make(services: Services) -> ProfileContainerViewModel { - let viewModel = ProfileContainerViewModel() - viewModel.bind(services: services) - return viewModel - } +final class ProfileContainerViewModel: ObservableObject { + @Published var isLoading = false + @Published var profileCardData = ProfileCardData() - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case .logout: - services.authenticator.logout() - case .loadProfileData: - self?.loadProfileData(dataService: services.dataService) - case .showIntercomMessenger: - DataService.showIntercomMessenger?() - case .deleteAccount: - print("delete account") - } - } - .store(in: &subscriptions) - } + var subscriptions = Set() - private func loadProfileData(dataService: DataService) { + func loadProfileData(dataService: DataService) { dataService.viewerPublisher().sink( receiveCompletion: { _ in }, receiveValue: { [weak self] viewer in @@ -44,30 +26,12 @@ extension ProfileContainerViewModel { } } -final class ProfileContainerViewModel: ObservableObject { - @Published var isLoading = false - @Published var profileCardData = ProfileCardData() - - enum Action { - case logout - case loadProfileData - case showIntercomMessenger - case deleteAccount - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init() {} -} - struct ProfileContainerView: View { - @ObservedObject private var viewModel: ProfileContainerViewModel - @State private var showLogoutConfirmation = false + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService - init(viewModel: ProfileContainerViewModel) { - self.viewModel = viewModel - } + @ObservedObject private var viewModel = ProfileContainerViewModel() + @State private var showLogoutConfirmation = false var body: some View { #if os(iOS) @@ -87,7 +51,7 @@ struct ProfileContainerView: View { Section { ProfileCard(data: viewModel.profileCardData) .onAppear { - viewModel.performActionSubject.send(.loadProfileData) + viewModel.loadProfileData(dataService: dataService) } } @@ -114,7 +78,7 @@ struct ProfileContainerView: View { if FeatureFlag.showAccountDeletion { NavigationLink( destination: ManageAccountView(handleAccountDeletion: { - viewModel.performActionSubject.send(.deleteAccount) + print("delete account") }) ) { Text("Manage Account") @@ -129,7 +93,7 @@ struct ProfileContainerView: View { Alert( title: Text("Are you sure you want to logout?"), primaryButton: .destructive(Text("Confirm")) { - viewModel.performActionSubject.send(.logout) + authenticator.logout() }, secondaryButton: .cancel() ) From d1f0e90e62560aeddb92f20029bd6ef1ba41b784 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 23:27:26 -0800 Subject: [PATCH 21/21] use env object for home feed view --- .../Binders/PrimaryContentCategory.swift | 7 +- .../Sources/Binders/RootViewModel.swift | 2 +- .../Sources/Binders/Views/HomeFeedView.swift | 139 ++++++------------ .../Binders/Views/PrimaryContentView.swift | 15 +- .../Binders/Views/ProfileContainerView.swift | 2 +- 5 files changed, 53 insertions(+), 112 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift index ea2e75b33..60ddf6863 100644 --- a/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/Binders/PrimaryContentCategory.swift @@ -1,9 +1,8 @@ import SwiftUI import Views -// TODO: maybe this can be removed?? enum PrimaryContentCategory: Identifiable, Hashable, Equatable { - case feed(viewModel: HomeFeedViewModel) + case feed case profile static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { @@ -38,8 +37,8 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { @ViewBuilder var destinationView: some View { switch self { - case let .feed(viewModel: viewModel): - HomeFeedView(viewModel: viewModel) + case .feed: + HomeFeedView() case .profile: ProfileContainerView() } diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index 662d83dbe..72ceaf526 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -145,7 +145,7 @@ public struct RootView: View { @ViewBuilder private var innerBody: some View { if authenticator.isLoggedIn { - PrimaryContentView(services: viewModel.services) + PrimaryContentView() .onAppear { viewModel.triggerPushNotificationRequestIfNeeded() } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift index 83bedbee2..f87f7eadc 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/HomeFeedView.swift @@ -6,40 +6,39 @@ import UserNotifications import Utils import Views -extension HomeFeedViewModel { - static func make(services: Services) -> HomeFeedViewModel { - let viewModel = HomeFeedViewModel() - viewModel.bind(services: services) - viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false) - return viewModel - } +final class HomeFeedViewModel: ObservableObject { + var currentDetailViewModel: LinkItemDetailViewModel? - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case let .refreshItems(query: query): - self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: true) - case let .loadItems(query): - self?.loadItems(dataService: services.dataService, searchQuery: query, isRefresh: false) - case let .archive(linkId): - self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: true) - case let .unarchive(linkId): - self?.setLinkArchived(dataService: services.dataService, linkId: linkId, archived: false) - case let .remove(linkId): - self?.removeLink(dataService: services.dataService, linkId: linkId) - case let .snooze(linkId, until, successMessage): - self?.snoozeUntil( - dataService: services.dataService, - linkId: linkId, - until: until, - successMessage: successMessage - ) - } + @Published var items = [FeedItem]() + @Published var isLoading = false + @Published var showPushNotificationPrimer = false + var cursor: String? + + // These are used to make sure we handle search result + // responses in the right order + var searchIdx = 0 + var receivedIdx = 0 + + var subscriptions = Set() + + init() {} + + func itemAppeared(item: FeedItem, searchQuery: String, dataService: DataService) { + if isLoading { return } + let itemIndex = items.firstIndex(where: { $0.id == item.id }) + let thresholdIndex = items.index(items.endIndex, offsetBy: -5) + + // Check if user has scrolled to the last five items in the list + if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { + loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: false) } - .store(in: &subscriptions) } - private func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { + func pushFeedItem(item: FeedItem) { + items.insert(item, at: 0) + } + + func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set dataService.clearHighlights() @@ -90,7 +89,7 @@ extension HomeFeedViewModel { .store(in: &subscriptions) } - private func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) { + func setLinkArchived(dataService: DataService, linkId: String, archived: Bool) { isLoading = true startNetworkActivityIndicator() @@ -120,7 +119,7 @@ extension HomeFeedViewModel { .store(in: &subscriptions) } - private func removeLink(dataService: DataService, linkId: String) { + func removeLink(dataService: DataService, linkId: String) { isLoading = true startNetworkActivityIndicator() @@ -146,7 +145,7 @@ extension HomeFeedViewModel { .store(in: &subscriptions) } - private func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) { + func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) { isLoading = true startNetworkActivityIndicator() @@ -178,55 +177,10 @@ extension HomeFeedViewModel { } } -// TODO: remove this view model -final class HomeFeedViewModel: ObservableObject { - var currentDetailViewModel: LinkItemDetailViewModel? - - @Published var items = [FeedItem]() - @Published var isLoading = false - @Published var showPushNotificationPrimer = false - var cursor: String? - - // These are used to make sure we handle search result - // responses in the right order - var searchIdx = 0 - var receivedIdx = 0 - - enum Action { - case refreshItems(query: String) - case loadItems(query: String) - case archive(linkId: String) - case unarchive(linkId: String) - case remove(linkId: String) - case snooze(linkId: String, until: Date, successMessage: String?) - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init() {} - - func itemAppeared(item: FeedItem, searchQuery: String) { - if isLoading { return } - let itemIndex = items.firstIndex(where: { $0.id == item.id }) - let thresholdIndex = items.index(items.endIndex, offsetBy: -5) - - // Check if user has scrolled to the last five items in the list - if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - performActionSubject.send(.loadItems(query: searchQuery)) - } - } - - func pushFeedItem(item: FeedItem) { - items.insert(item, at: 0) - } -} - struct HomeFeedView: View { -// @EnvironmentObject var authenticator: Authenticator -// @EnvironmentObject var dataService: DataService + @EnvironmentObject var dataService: DataService - @ObservedObject private var viewModel: HomeFeedViewModel + @ObservedObject private var viewModel = HomeFeedViewModel() @State private var selectedLinkItem: FeedItem? @State private var searchQuery = "" @State private var itemToRemove: FeedItem? @@ -234,10 +188,6 @@ struct HomeFeedView: View { @State private var snoozePresented = false @State private var itemToSnooze: FeedItem? - init(viewModel: HomeFeedViewModel) { - self.viewModel = viewModel - } - @ViewBuilder var conditionalInnerBody: some View { #if os(iOS) if #available(iOS 15.0, *) { @@ -301,14 +251,14 @@ struct HomeFeedView: View { .opacity(0) .buttonStyle(PlainButtonStyle()) .onAppear { - viewModel.itemAppeared(item: item, searchQuery: searchQuery) + viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) } FeedCard(item: item) }.contextMenu { if !item.isArchived { Button(action: { withAnimation(.linear(duration: 0.4)) { - viewModel.performActionSubject.send(.archive(linkId: item.id)) + viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: true) if item == selectedLinkItem { selectedLinkItem = nil } @@ -317,7 +267,7 @@ struct HomeFeedView: View { } else { Button(action: { withAnimation(.linear(duration: 0.4)) { - viewModel.performActionSubject.send(.unarchive(linkId: item.id)) + viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: false) } }, label: { Label("Unarchive", systemImage: "tray.and.arrow.down.fill") }) } @@ -335,7 +285,7 @@ struct HomeFeedView: View { if !item.isArchived { Button { withAnimation(.linear(duration: 0.4)) { - viewModel.performActionSubject.send(.archive(linkId: item.id)) + viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: true) } } label: { Label("Archive", systemImage: "archivebox") @@ -343,7 +293,7 @@ struct HomeFeedView: View { } else { Button { withAnimation(.linear(duration: 0.4)) { - viewModel.performActionSubject.send(.unarchive(linkId: item.id)) + viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: false) } } label: { Label("Unarchive", systemImage: "tray.and.arrow.down.fill") @@ -365,7 +315,7 @@ struct HomeFeedView: View { Button("Remove Link", role: .destructive) { if let itemToRemove = itemToRemove { withAnimation { - viewModel.performActionSubject.send(.remove(linkId: itemToRemove.id)) + viewModel.removeLink(dataService: dataService, linkId: itemToRemove.id) } } self.itemToRemove = nil @@ -417,8 +367,11 @@ struct HomeFeedView: View { } .formSheet(isPresented: $snoozePresented) { SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) { - viewModel.performActionSubject.send( - .snooze(linkId: $0.feedItemId, until: $0.snoozeUntilDate, successMessage: $0.successMessage) + viewModel.snoozeUntil( + dataService: dataService, + linkId: $0.feedItemId, + until: $0.snoozeUntilDate, + successMessage: $0.successMessage ) } } @@ -459,7 +412,7 @@ struct HomeFeedView: View { } private func refresh() { - viewModel.performActionSubject.send(.refreshItems(query: searchQuery)) + viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) } } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift index e0aa59060..422e770cb 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift @@ -4,33 +4,22 @@ import SwiftUI import Views public struct PrimaryContentView: View { - let homeFeedViewModel: HomeFeedViewModel - - public init(services: Services) { - self.homeFeedViewModel = HomeFeedViewModel.make(services: services) - } - public var body: some View { #if os(iOS) if UIDevice.isIPad { regularView } else { - compactView + HomeFeedView() } #elseif os(macOS) regularView #endif } - // iphone view container - private var compactView: some View { - HomeFeedView(viewModel: homeFeedViewModel) - } - // ipad and mac view container private var regularView: some View { let categories = [ - PrimaryContentCategory.feed(viewModel: homeFeedViewModel), + PrimaryContentCategory.feed, PrimaryContentCategory.profile ] diff --git a/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift index 907ba3418..111483915 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/ProfileContainerView.swift @@ -67,7 +67,7 @@ struct ProfileContainerView: View { #if os(iOS) Button( action: { - viewModel.performActionSubject.send(.showIntercomMessenger) + DataService.showIntercomMessenger?() }, label: { Text("Feedback") } )