diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FollowingViewModal.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FollowingViewModal.swift new file mode 100644 index 000000000..b9c42c539 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FollowingViewModal.swift @@ -0,0 +1,70 @@ +// swiftlint:disable line_length + +import Foundation +import Models +import Services +import SwiftUI +import Views + +public struct FollowingViewModal: View { + @Environment(\.dismiss) private var dismiss + + let message: String = """ + We've created a new place for all your newsletters and feeds called Following. You can control the destination of + new items by changing the destination for your subscriptions in the Subscriptions view of your settings. By default + your existing newsletters will go into your library and your existing feeds will go into Following. + + From the library you can swipe items left to right to move them into your library. In the reader view you can tap the + bookmark icon on the toolbar to move items into your library. + + If you don't need the following tab you can disable it from the filters view in your settings. + + - [Learn more about the following](https://docs.omnivore.app/using/following.html) + + - [Tell your friends about Omnivore](https://omnivore.app/about) + + """ + + var closeButton: some View { + Button(action: { + dismiss() + }, label: { + ZStack { + Circle() + .foregroundColor(Color.circleButtonBackground) + .frame(width: 30, height: 30) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color.circleButtonForeground) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.bold)) + .frame(width: 12, height: 12) + } + }) + } + + public var body: some View { + HStack { + Text("Your new Following tab") + .font(Font.system(size: 20, weight: .bold)) + Spacer() + closeButton + } + .padding(.top, 16) + .padding(.horizontal, 16) + + List { + Section { + let parsedMessage = try? AttributedString(markdown: message, + options: .init(interpretedSyntax: .inlineOnly)) + Text(parsedMessage ?? "") + .multilineTextAlignment(.leading) + .foregroundColor(Color.appGrayTextContrast) + .accentColor(.blue) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 16) + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 7814e5ad8..24b1c6b61 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -79,30 +79,73 @@ struct FiltersHeader: View { struct EmptyState: View { @ObservedObject var viewModel: HomeFeedViewModel + @EnvironmentObject var dataService: DataService + + @State var showSendNewslettersAlert = false + + var followingEmptyState: some View { + VStack(alignment: .center, spacing: 20) { + if viewModel.stopUsingFollowingPrimer { + VStack(spacing: 10) { + Image.relaxedSlothLight + Text("You are all caught up.").foregroundColor(Color.extensionTextSubtle) + Button(action: { + Task { + await viewModel.loadItems(dataService: dataService, isRefresh: true, loadingBarStyle: .simple) + } + }, label: { Text("Refresh").bold() }) + .foregroundColor(Color.blue) + } + } else { + Text("You don't have any Feed items.") + .font(Font.system(size: 18, weight: .bold)) + + Text("Add an RSS/Atom feed") + .foregroundColor(Color.blue) + .onTapGesture { + viewModel.showAddFeedView = true + } + + Text("Send your newsletters to following") + .foregroundColor(Color.blue) + .onTapGesture { + showSendNewslettersAlert = true + } + + Text("Hide the Following tab") + .foregroundColor(Color.blue) + .onTapGesture { + viewModel.showHideFollowingAlert = true + } + } + } + + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() + .alert("Update newsletter destination", isPresented: $showSendNewslettersAlert, actions: { + Button(action: { + Task { + await viewModel.modifyingNewsletterDestinationToFollowing(dataService: dataService) + } + }, label: { Text("OK") }) + Button(LocalText.cancelGeneric, role: .cancel) { showSendNewslettersAlert = false } + }, message: { + // swiftlint:disable:next line_length + Text("Your email address destination folders will be modified to send to this tab.\n\nAll new newsletters will appear here. You can modify the destination for each individual email address and subscription in your settings.") + }) + } var body: some View { - if viewModel.currentFolder == "following" { + if viewModel.isModifyingNewsletterDestination { return AnyView( - VStack(alignment: .center, spacing: 20) { - Text("You don't have any Feed items.") - .font(Font.system(size: 18, weight: .bold)) - - Text("Add an RSS/Atom feed") - .foregroundColor(Color.blue) - .onTapGesture { - viewModel.showAddFeedView = true - } - - Text("Hide the Following tab") - .foregroundColor(Color.blue) - .onTapGesture { - viewModel.showHideFollowingAlert = true - } - } - .frame(minHeight: 400) - .frame(maxWidth: .infinity) - .padding() + VStack { + Text("Modifying newsletter destinations...") + ProgressView() + }.frame(maxWidth: .infinity, maxHeight: .infinity) ) + } else if viewModel.currentFolder == "following" { + return AnyView(followingEmptyState) } else { return AnyView(Group { Spacer() @@ -143,7 +186,6 @@ struct AnimatingCellHeight: AnimatableModifier { @State var hasHighlightMutations = false @State var searchPresented = false @State var showAddLinkView = false - @State var showAddFeedView = false @State var isListScrolled = false @State var listTitle = "" @State var isEditMode: EditMode = .inactive @@ -184,7 +226,6 @@ struct AnimatingCellHeight: AnimatableModifier { isListScrolled: $isListScrolled, prefersListLayout: $prefersListLayout, isEditMode: $isEditMode, - showAddFeedView: $showAddFeedView, selection: $selection, viewModel: viewModel, showFeatureCards: showFeatureCards @@ -240,10 +281,10 @@ struct AnimatingCellHeight: AnimatableModifier { .sheet(item: $viewModel.itemForHighlightsView) { item in NotebookView(viewModel: NotebookViewModel(item: item), hasHighlightMutations: $hasHighlightMutations) } - .sheet(isPresented: $showAddFeedView) { + .sheet(isPresented: $viewModel.showAddFeedView) { NavigationView { LibraryAddFeedView(dismiss: { - showAddFeedView = false + viewModel.showAddFeedView = false }, toastOperationHandler: nil) } } @@ -295,6 +336,11 @@ struct AnimatingCellHeight: AnimatableModifier { if viewModel.appliedFilter == nil { viewModel.setDefaultFilter() } + // Once the user has seen at least one following item we stop displaying the + // initial help view + if viewModel.currentFolder == "following", viewModel.fetcher.items.count > 0 { + viewModel.stopUsingFollowingPrimer = true + } } .environment(\.editMode, self.$isEditMode) .navigationBarTitleDisplayMode(.inline) @@ -346,7 +392,7 @@ struct AnimatingCellHeight: AnimatableModifier { if viewModel.currentFolder == "inbox" { showAddLinkView = true } else if viewModel.currentFolder == "following" { - showAddFeedView = true + viewModel.showAddFeedView = true } }, label: { @@ -405,7 +451,6 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isListScrolled: Bool @Binding var prefersListLayout: Bool @Binding var isEditMode: EditMode - @Binding var showAddFeedView: Bool @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel @@ -745,8 +790,16 @@ struct AnimatingCellHeight: AnimatableModifier { } } - if viewModel.showLoadingBar { + if viewModel.showLoadingBar == .redacted { redactedItems + } else if viewModel.showLoadingBar == .simple { + VStack { + ProgressView() + } + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() + .listRowSeparator(.hidden, edges: .all) } else if viewModel.fetcher.items.isEmpty { EmptyState(viewModel: viewModel) .listRowSeparator(.hidden, edges: .all) @@ -906,13 +959,21 @@ struct AnimatingCellHeight: AnimatableModifier { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) { - if viewModel.showLoadingBar { + if viewModel.showLoadingBar == .redacted { ForEach(fakeLibraryItems(dataService: dataService), id: \.id) { item in GridCard(item: item) .aspectRatio(1.0, contentMode: .fill) .background(Color.systemBackground) .cornerRadius(6) }.redacted(reason: .placeholder) + } else if viewModel.showLoadingBar == .simple { + VStack { + ProgressView() + } + .frame(minHeight: 400) + .frame(maxWidth: .infinity) + .padding() + .listRowSeparator(.hidden, edges: .all) } else { if !viewModel.fetcher.items.isEmpty { ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.id) { idx, item in diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 425b5465f..9f89b307b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -5,6 +5,12 @@ import SwiftUI import Utils import Views +enum LoadingBarStyle { + case none + case redacted + case simple +} + @MainActor final class HomeFeedViewModel: NSObject, ObservableObject { let filterKey: String @ObservedObject var fetcher: LibraryItemFetcher @@ -16,7 +22,7 @@ import Views @Published var itemForHighlightsView: Models.LibraryItem? @Published var linkRequest: LinkRequest? @Published var presentWebContainer = false - @Published var showLoadingBar = true + @Published var showLoadingBar = LoadingBarStyle.redacted @Published var selectedItem: Models.LibraryItem? @Published var linkIsActive = false @@ -37,7 +43,10 @@ import Views @State var lastMoreFetched: Date? @State var lastFiltersFetched: Date? + @State var isModifyingNewsletterDestination = false + @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false + @AppStorage(UserDefaultKey.stopUsingFollowingPrimer.rawValue) var stopUsingFollowingPrimer = false @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false @Published var appliedFilter: InternalFilter? { @@ -191,9 +200,9 @@ import Views } } - func loadItems(dataService: DataService, isRefresh: Bool, forceRemote: Bool = false) async { + func loadItems(dataService: DataService, isRefresh: Bool, forceRemote: Bool = false, loadingBarStyle: LoadingBarStyle? = nil) async { isLoading = true - showLoadingBar = isRefresh + showLoadingBar = isRefresh ? loadingBarStyle ?? .redacted : .none if let filterState = filterState { await fetcher.loadItems( @@ -205,7 +214,7 @@ import Views } isLoading = false - showLoadingBar = false + showLoadingBar = .none } func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [Models.LibraryItem] { @@ -325,4 +334,34 @@ import Views func findFilter(_: DataService, named: String) -> InternalFilter? { filters.first(where: { $0.name == named }) } + + func modifyingNewsletterDestinationToFollowing(dataService: DataService) async { + isModifyingNewsletterDestination = true + do { + var errorCount = 0 + let objectIDs = try await dataService.newsletterEmails() + let newsletters = await dataService.viewContext.perform { + let newsletters = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } + return newsletters + } + + for newsletter in newsletters { + if let emailId = newsletter.emailId, newsletter.folder != "following" { + do { + try await dataService.updateNewsletterEmail(emailID: emailId, folder: "following") + } catch { + print("error updating newsletter: ", error) + errorCount += 1 + } + } + } + if errorCount > 0 { + snackbar("There was an error modifying \(errorCount) of your emails") + } else { + snackbar("Email destination modified") + } + } catch { + snackbar("Error modifying emails") + } + } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 1fcf44107..6fa328866 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -21,9 +21,6 @@ struct LibraryTabView: View { @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false @AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox" - @AppStorage(UserDefaultKey.followingPrimerDisplayed.rawValue) var followingPrimerDisplayed = false - - @State var showFollowingPrimer = false @State var showExpandedAudioPlayer = false private let syncManager = LibrarySyncManager() @@ -74,14 +71,6 @@ struct LibraryTabView: View { var body: some View { VStack(spacing: 0) { - if showFollowingPrimer { - PresentationLink(transition: UIDevice.isIPad ? .popover : .sheet(detents: [.medium]), isPresented: $showFollowingPrimer) { - FollowingViewModal() - } label: { - EmptyView() - } - } - TabView(selection: $selectedTab) { if !hideFollowingTab { NavigationView { diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index 8aa1cfd60..8e8953575 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -28,7 +28,7 @@ public enum UserDefaultKey: String { case recentSearchTerms case audioPlayerExpanded case themeName - case followingPrimerDisplayed + case stopUsingFollowingPrimer case notificationsEnabled case deviceTokenID case userWordsPerMinute diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.swift b/apple/OmnivoreKit/Sources/Views/Images/Images.swift index 8dd6c6fe4..f90d34ede 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.swift +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.swift @@ -30,6 +30,10 @@ public extension Image { static var readerSettings: Image { Image("reader-settings", bundle: .module) } static var utilityMenu: Image { Image("utility-menu", bundle: .module) } + static var relaxedSlothLight: Image { + Color.isDarkMode ? Image("relaxed-sloth-dark", bundle: .module) : Image("relaxed-sloth-light", bundle: .module) + } + static var addLink: Image { Image("add-link", bundle: .module) } static var selectMultiple: Image { Image("select-multiple", bundle: .module) } static var magnifyingGlass: Image { Image("magnifying-glass", bundle: .module) } diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/Contents.json new file mode 100644 index 000000000..8df8ad011 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "relaxed-sloth-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sloth 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sloth 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/relaxed-sloth-dark.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/relaxed-sloth-dark.png new file mode 100644 index 000000000..5a9e8c0ef Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/relaxed-sloth-dark.png differ diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 1.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 1.png new file mode 100644 index 000000000..8f0d3c908 Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 1.png differ diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 2.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 2.png new file mode 100644 index 000000000..c18f9dfcc Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 2.png differ diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/Contents.json new file mode 100644 index 000000000..0cd111630 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "sloth 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sloth 2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sloth 3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 1.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 1.png new file mode 100644 index 000000000..02f0b0bbf Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 1.png differ diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 2.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 2.png new file mode 100644 index 000000000..f838edbc2 Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 2.png differ diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 3.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 3.png new file mode 100644 index 000000000..efa1a80e9 Binary files /dev/null and b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 3.png differ