Merge pull request #3297 from omnivore-app/feat/ios-empty-following
feat/ios empty following
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String>
|
||||
@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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -28,7 +28,7 @@ public enum UserDefaultKey: String {
|
||||
case recentSearchTerms
|
||||
case audioPlayerExpanded
|
||||
case themeName
|
||||
case followingPrimerDisplayed
|
||||
case stopUsingFollowingPrimer
|
||||
case notificationsEnabled
|
||||
case deviceTokenID
|
||||
case userWordsPerMinute
|
||||
|
||||
@ -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) }
|
||||
|
||||
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/Contents.json
vendored
Normal file
23
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 1.png
vendored
Normal file
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 2.png
vendored
Normal file
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-dark.imageset/sloth 2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@ -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
|
||||
}
|
||||
}
|
||||
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 1.png
vendored
Normal file
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 2.png
vendored
Normal file
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 3.png
vendored
Normal file
BIN
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/relaxed-sloth-light.imageset/sloth 3.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Reference in New Issue
Block a user