diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index 9cb448341..f64616d46 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -29,21 +29,6 @@ public final class RootViewModel: ObservableObject { registerFonts() } - func updateWaitlistStatus() { - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { [weak self] viewer in - guard let self = self else { return } - let isWaitlisted = viewer.isWaitlisted - self.services.authenticator.updateWaitlistStatus(isWaitlistedUser: isWaitlisted) - } - ) - .store(in: &subscriptions) - } - func configurePDFProvider(pdfViewerProvider: @escaping (URL, PDFViewerViewModel) -> AnyView) { PDFProvider.pdfViewerProvider = { [weak self] url, feedItem in guard let self = self else { return AnyView(Text("")) } @@ -133,8 +118,6 @@ public struct RootView: View { @ObservedObject private var viewModel: RootViewModel @ObservedObject private var authenticator: Authenticator - private var primaryViewModel: PrimaryContentViewModel - public init( pdfViewerProvider: ((URL, PDFViewerViewModel) -> AnyView)?, intercomProvider: IntercomProvider? @@ -142,7 +125,6 @@ public struct RootView: View { let rootViewModel = RootViewModel() self.viewModel = rootViewModel self.authenticator = rootViewModel.services.authenticator - self.primaryViewModel = PrimaryContentViewModel.make(services: rootViewModel.services) #if DEBUG if CommandLine.arguments.contains("--uitesting") { @@ -163,34 +145,30 @@ public struct RootView: View { @ViewBuilder private var innerBody: some View { if authenticator.isLoggedIn { - if authenticator.isWaitlisted { - WaitlistView(viewModel: WaitlistViewModel.make(services: viewModel.services)) - } else { - PrimaryContentView(viewModel: primaryViewModel) - .onAppear { - viewModel.updateWaitlistStatus() - viewModel.triggerPushNotificationRequestIfNeeded() + PrimaryContentView(services: viewModel.services) + .onAppear { + viewModel.triggerPushNotificationRequestIfNeeded() + } + #if os(iOS) + .fullScreenCover(item: $viewModel.webLinkPath, content: { safariLinkPath in + NavigationView { + FullScreenWebAppView( + viewModel: viewModel.webAppWrapperViewModel(webLinkPath: safariLinkPath.path), + handleClose: { viewModel.webLinkPath = nil } + ) } - #if os(iOS) - .fullScreenCover(item: $viewModel.webLinkPath, content: { safariLinkPath in - NavigationView { - FullScreenWebAppView( - viewModel: viewModel.webAppWrapperViewModel(webLinkPath: safariLinkPath.path), - handleClose: { viewModel.webLinkPath = nil } - ) - } - }) - #endif - .snackBar( - isShowing: $viewModel.showSnackbar, - text: Text(viewModel.snackbarMessage ?? "") - ) - #if os(iOS) - .customAlert(isPresented: $viewModel.showPushNotificationPrimer) { - pushNotificationPrimerView - } - #endif - } + }) + #endif + .snackBar( + isShowing: $viewModel.showSnackbar, + text: Text(viewModel.snackbarMessage ?? "") + ) + #if os(iOS) + .customAlert(isPresented: $viewModel.showPushNotificationPrimer) { + pushNotificationPrimerView + } + #endif + } else { WelcomeView(viewModel: WelcomeViewModel.make(services: viewModel.services)) .accessibilityElement() diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift index 820cbf624..5f4541645 100644 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift +++ b/apple/OmnivoreKit/Sources/Binders/Scenes/HomeFeedScene.swift @@ -11,6 +11,10 @@ extension HomeFeedViewModel { 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 diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/PrimaryContentScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/PrimaryContentScene.swift deleted file mode 100644 index 2b30e926d..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/PrimaryContentScene.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Services -import SwiftUI -import Views - -extension PrimaryContentViewModel { - static func make(services: Services) -> PrimaryContentViewModel { - let viewModel = PrimaryContentViewModel( - homeFeedViewModel: HomeFeedViewModel.make(services: services), - profileContainerViewModel: ProfileContainerViewModel.make(services: services) - ) - viewModel.bind(services: services) - return viewModel - } - - func bind(services _: Services) { - performActionSubject.sink { action in - switch action { - case .nothing: - break - } - } - .store(in: &subscriptions) - } -} diff --git a/apple/OmnivoreKit/Sources/Binders/Scenes/WaitlistScene.swift b/apple/OmnivoreKit/Sources/Binders/Scenes/WaitlistScene.swift deleted file mode 100644 index 06230803b..000000000 --- a/apple/OmnivoreKit/Sources/Binders/Scenes/WaitlistScene.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -extension WaitlistViewModel { - static func make(services: Services) -> WaitlistViewModel { - let viewModel = WaitlistViewModel() - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case .logout: - services.authenticator.logout() - case .checkStatus: - self?.updateWaitlistStatus(services: services) - } - } - .store(in: &subscriptions) - } - - private func updateWaitlistStatus(services: Services) { - services.dataService.viewerPublisher().sink( - receiveCompletion: { completion in - guard case let .failure(error) = completion else { return } - print(error) - }, - receiveValue: { viewer in - let isWaitlisted = viewer.isWaitlisted - services.authenticator.updateWaitlistStatus(isWaitlistedUser: isWaitlisted) - } - ) - .store(in: &subscriptions) - } -} diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift similarity index 57% rename from apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentView.swift rename to apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift index 2298b168c..3c6fa23be 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/PrimaryContentView.swift @@ -1,34 +1,15 @@ -import Combine import Models +import Services import SwiftUI - -public final class PrimaryContentViewModel: ObservableObject { - let categories: [PrimaryContentCategory] - - public enum Action { - case nothing - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init( - homeFeedViewModel: HomeFeedViewModel, - profileContainerViewModel: ProfileContainerViewModel - ) { - self.categories = [ - .feed(viewModel: homeFeedViewModel), - .profile(viewModel: profileContainerViewModel) - ] - } -} +import Views public struct PrimaryContentView: View { - @ObservedObject private var viewModel: PrimaryContentViewModel - @State private var currentTab = 0 + let homeFeedViewModel: HomeFeedViewModel + let profileContainerViewModel: ProfileContainerViewModel - public init(viewModel: PrimaryContentViewModel) { - self.viewModel = viewModel + public init(services: Services) { + self.homeFeedViewModel = HomeFeedViewModel.make(services: services) + self.profileContainerViewModel = ProfileContainerViewModel.make(services: services) } public var body: some View { @@ -45,27 +26,23 @@ public struct PrimaryContentView: View { // iphone view container private var compactView: some View { - TabView(selection: $currentTab) { - ForEach(viewModel.categories.indices, id: \.self) { index in - viewModel.categories[index].destinationView - .tabItem { - TabIcon(isSelected: currentTab == index, primaryContentCategory: viewModel.categories[index]) - } - .tag(index) - } - } - .accentColor(.appGrayTextContrast) + HomeFeedView(viewModel: homeFeedViewModel) } // ipad and mac view container private var regularView: some View { - NavigationView { + let categories = [ + PrimaryContentCategory.feed(viewModel: homeFeedViewModel), + PrimaryContentCategory.profile(viewModel: profileContainerViewModel) + ] + + return NavigationView { // The first column is the sidebar. - PrimaryContentSidebar(categories: viewModel.categories) + PrimaryContentSidebar(categories: categories) .navigationTitle("Categories") // Initial Content of second column - if let destinationView = viewModel.categories.first?.destinationView { + if let destinationView = categories.first?.destinationView { destinationView } else { Text("Select a Category") diff --git a/apple/OmnivoreKit/Sources/Models/UserProfile.swift b/apple/OmnivoreKit/Sources/Models/UserProfile.swift index 4d9e64ca7..d3588df5c 100644 --- a/apple/OmnivoreKit/Sources/Models/UserProfile.swift +++ b/apple/OmnivoreKit/Sources/Models/UserProfile.swift @@ -50,19 +50,16 @@ public struct Viewer { public let username: String public let name: String public let profileImageURL: String? - public let isWaitlisted: Bool public init( username: String, name: String, profileImageURL: String?, - isWaitlisted: Bool, userID: String ) { self.username = username self.name = name self.profileImageURL = profileImageURL - self.isWaitlisted = isWaitlisted self.userID = userID } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index 1b0d29fb3..d973530bb 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -14,7 +14,6 @@ public final class Authenticator: ObservableObject { } @Published public internal(set) var isLoggedIn: Bool - @Published public internal(set) var isWaitlisted = false let networker: Networker @@ -58,10 +57,6 @@ public final class Authenticator: ObservableObject { return !authToken.isEmpty } - public func updateWaitlistStatus(isWaitlistedUser: Bool) { - isWaitlisted = isWaitlistedUser - } - private func clearCookies() { HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift index 2f0089b4d..7b91e2c50 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ViewerPublisher.swift @@ -30,7 +30,6 @@ extension DataService { profileImageURL: try $0.profile( selection: .init { try $0.pictureUrl() } ), - isWaitlisted: try !($0.isFullUser() ?? false), userID: try $0.id() ) } diff --git a/apple/OmnivoreKit/Sources/Binders/UIViewControllerExtensions.swift b/apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift similarity index 100% rename from apple/OmnivoreKit/Sources/Binders/UIViewControllerExtensions.swift rename to apple/OmnivoreKit/Sources/Utils/UIViewControllerExtensions.swift diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.swift b/apple/OmnivoreKit/Sources/Views/Images/Images.swift index b2a966e51..9f9526a5a 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.swift +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.swift @@ -15,5 +15,6 @@ extension Image { static var homeTab: Image { Image("_homeTab", bundle: .module) } static var homeTabSelected: Image { Image("_homeTabSelected", bundle: .module) } static var profileTab: Image { Image("_profileTab", bundle: .module) } + static var profile: Image { Image("_profile", bundle: .module) } static var profileTabSelected: Image { Image("_profileTabSelected", bundle: .module) } } diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/Contents.json new file mode 100644 index 000000000..8eb42e0b3 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "user-circle-gear.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/user-circle-gear.svg b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/user-circle-gear.svg new file mode 100644 index 000000000..89da59a7c --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profile.imageset/user-circle-gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift index 5c9010888..cea24590e 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/HomeFeedView.swift @@ -6,6 +6,7 @@ import Utils public final class HomeFeedViewModel: ObservableObject { let detailViewModelCreator: (FeedItem) -> LinkItemDetailViewModel var currentDetailViewModel: LinkItemDetailViewModel? + public var profileContainerViewModel: ProfileContainerViewModel? @Published public var items = [FeedItem]() @Published public var isLoading = false @@ -256,10 +257,26 @@ public struct HomeFeedView: View { public var body: some View { #if os(iOS) - if UIDevice.isIPhone { + if UIDevice.isIPhone, let profileContainerViewModel = viewModel.profileContainerViewModel { NavigationView { conditionalInnerBody + .toolbar { + ToolbarItem { + NavigationLink( + destination: { + ProfileContainerView(viewModel: profileContainerViewModel) + }, + label: { + Image.profile + .resizable() + .frame(width: 26, height: 26) + .padding() + } + ) + } + } } + .accentColor(.appGrayTextContrast) } else { conditionalInnerBody } diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift index 0b08f0c3b..13919d14e 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/PrimaryContentCategory.swift @@ -1,14 +1,14 @@ import SwiftUI -enum PrimaryContentCategory: Identifiable, Hashable, Equatable { +public enum PrimaryContentCategory: Identifiable, Hashable, Equatable { case feed(viewModel: HomeFeedViewModel) case profile(viewModel: ProfileContainerViewModel) - static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { + public static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool { lhs.id == rhs.id } - var id: String { + public var id: String { title } @@ -39,11 +39,11 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { } } - var listLabel: some View { + public var listLabel: some View { Label { Text(title) } icon: { image.renderingMode(.template) } } - @ViewBuilder var destinationView: some View { + @ViewBuilder public var destinationView: some View { switch self { case let .feed(viewModel: viewModel): HomeFeedView(viewModel: viewModel) @@ -52,24 +52,7 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { } } - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(id) } } - -struct TabIcon: View { - let isSelected: Bool - let primaryContentCategory: PrimaryContentCategory - - var body: some View { - if isSelected { - Label { - Text(primaryContentCategory.title) - } icon: { - primaryContentCategory.selectedImage - } - } else { - primaryContentCategory.listLabel - } - } -} diff --git a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift index 9ac0769d7..3c3a9fc16 100644 --- a/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift +++ b/apple/OmnivoreKit/Sources/Views/PrimaryContainerViews/Profile/ProfileContainerView.swift @@ -30,16 +30,8 @@ public struct ProfileContainerView: View { public var body: some View { #if os(iOS) - if UIDevice.isIPhone { - NavigationView { - Form { - innerBody - } - } - } else { - Form { - innerBody - } + Form { + innerBody } #elseif os(macOS) List { diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/WaitlistView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/WaitlistView.swift deleted file mode 100644 index 2422c9cf0..000000000 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/WaitlistView.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Combine -import Models -import SwiftUI - -public final class WaitlistViewModel: ObservableObject { - public enum Action { - case logout - case checkStatus - } - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - - public init() {} -} - -public struct WaitlistView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var viewModel: WaitlistViewModel - - public init(viewModel: WaitlistViewModel) { - self.viewModel = viewModel - } - - @ViewBuilder func userInteractiveView(width: CGFloat) -> some View { - waitlistButtonView - .frame(width: width) - .zIndex(2) - } - - var titleLogo: some View { - Image.omnivoreTitleLogo - .renderingMode(.template) - .foregroundColor(.appGrayTextContrast) - .frame(height: 40) - } - - var waitlistButtonView: some View { - VStack(alignment: .center, spacing: 32) { - Text("Your username has been reserved. We will inform you by email when we open up Omnivore to more users.") - .font(.appHeadline) - .multilineTextAlignment(.center) - .frame(maxWidth: 300) - - BorderedButton(color: .appGrayTextContrast, text: "Check Status") { - viewModel.performActionSubject.send(.checkStatus) - } - .frame(width: 220) - - BorderedButton(color: .appGrayTextContrast, text: "Logout") { - viewModel.performActionSubject.send(.logout) - } - .frame(width: 220) - } - .padding(.horizontal, horizontalSizeClass == .compact ? 16 : 80) - .padding(.top, horizontalSizeClass == .compact ? 16 : 0) - } - - @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 - ZStack(alignment: .leading) { - Color.systemBackground - .edgesIgnoringSafeArea(.all) - - if geometry.size.width < geometry.size.height { - VStack { - Color.appDeepBackground.frame(height: 100) - Spacer() - } - .edgesIgnoringSafeArea(.all) - } - - VStack { - if geometry.size.width < geometry.size.height { - RegistrationHeroImageView( - tapGestureHandler: {} - ) - } - userInteractiveView(width: geometry.size.width) - Spacer() - } - } - } - } else { - GeometryReader { geometry in - ZStack(alignment: .leading) { - splitColorBackground(width: geometry.size.width) - - VStack { - titleLogo - Spacer() - } - .padding() - - HStack(spacing: 0) { - userInteractiveView(width: geometry.size.width * 0.5) - largeBackgroundImage(width: geometry.size.width * 0.5) - } - } - } - } - } - - public var body: some View { - primaryContent() - } -} diff --git a/apple/Sources/MainApp.swift b/apple/Sources/MainApp.swift index 78536872c..9bf3d593c 100644 --- a/apple/Sources/MainApp.swift +++ b/apple/Sources/MainApp.swift @@ -42,9 +42,6 @@ struct MainApp: App { WindowGroup { RootView(pdfViewerProvider: nil, intercomProvider: nil) } -// Settings { -// SettingsView() -// } #endif } @@ -52,18 +49,3 @@ struct MainApp: App { AnyView(PDFViewer(pdfURL: url, viewModel: viewModel)) } } - -struct SettingsView: View { - var appVersion: String { - Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - } - - var body: some View { - VStack { - Text("Omnivore") - .font(.largeTitle) - Text("Omnivore Version: \(appVersion)") - } - .frame(width: 600, height: 600) - } -}